diff --git a/doc/source/user/support-matrix.ini b/doc/source/user/support-matrix.ini index 95a2f7b7b669..7c94d29fc434 100644 --- a/doc/source/user/support-matrix.ini +++ b/doc/source/user/support-matrix.ini @@ -674,6 +674,10 @@ driver-impl-ironic=missing driver-impl-libvirt-vz-vm=complete driver-impl-libvirt-vz-ct=complete driver-impl-powervm=complete +driver-notes-powervm=When using the localdisk disk driver, snapshot is only + supported if I/O is being hosted by the management partition. If hosting I/O + on traditional VIOS, we are limited by the fact that a VSCSI device can't be + mapped to two partitions (the VIOS and the management) at once. [operation.suspend] title=Suspend instance diff --git a/nova/conf/powervm.py b/nova/conf/powervm.py index f18e79c28f50..50cd6aa8cd9f 100644 --- a/nova/conf/powervm.py +++ b/nova/conf/powervm.py @@ -1,5 +1,4 @@ # Copyright 2018 IBM Corporation -# 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 @@ -34,8 +33,26 @@ powervm_opts = [ compute power given to each vCPU. E.g. A value of 1.0 means a whole physical processor, whereas 0.05 means 1/20th of a physical processor. - """ - ), + """), + cfg.StrOpt('disk_driver', + choices=['localdisk', 'ssp'], ignore_case=True, + default='localdisk', + help="""The disk driver to use for PowerVM disks. PowerVM + provides support for localdisk and PowerVM Shared Storage + Pool disk drivers. + + Related options: + + * volume_group_name - required when using localdisk + + """), + cfg.StrOpt('volume_group_name', + default='', + help='Volume Group to use for block device operations. If ' + 'disk_driver is localdisk, then this attribute must be ' + 'specified. It is strongly recommended NOT to use ' + 'rootvg since that is used by the management partition ' + 'and filling it will cause failures.'), ] diff --git a/nova/exception.py b/nova/exception.py index 4c844347f0f6..b6365b36bdbb 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -2269,3 +2269,8 @@ class DeviceDeletionException(NovaException): msg_fmt = _("Device %(devpath)s is still present on the management " "partition after attempting to delete it. Polled %(polls)d " "times over %(timeout)d seconds.") + + +class OptRequiredIfOtherOptValue(NovaException): + msg_fmt = _("The %(then_opt)s option is required if %(if_opt)s is " + "specified as '%(if_value)s'.") diff --git a/nova/tests/unit/virt/powervm/disk/test_localdisk.py b/nova/tests/unit/virt/powervm/disk/test_localdisk.py new file mode 100644 index 000000000000..69d1e156f03b --- /dev/null +++ b/nova/tests/unit/virt/powervm/disk/test_localdisk.py @@ -0,0 +1,312 @@ +# Copyright 2015, 2018 IBM Corp. +# +# 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 fixtures +import mock + +from nova import exception +from nova import test +from pypowervm import const as pvm_const +from pypowervm.tasks import storage as tsk_stg +from pypowervm.utils import transaction as pvm_tx +from pypowervm.wrappers import storage as pvm_stg +from pypowervm.wrappers import virtual_io_server as pvm_vios + +from nova.tests import uuidsentinel as uuids +from nova.virt.powervm.disk import driver as disk_dvr +from nova.virt.powervm.disk import localdisk + + +class TestLocalDisk(test.NoDBTestCase): + """Unit Tests for the LocalDisk storage driver.""" + + def setUp(self): + super(TestLocalDisk, self).setUp() + self.adpt = mock.Mock() + + # The mock VIOS needs to have scsi_mappings as a list. Internals are + # set by individual test cases as needed. + smaps = [mock.Mock()] + self.vio_wrap = mock.create_autospec( + pvm_vios.VIOS, instance=True, scsi_mappings=smaps, + uuid='vios-uuid') + + # Return the mgmt uuid. + self.mgmt_uuid = self.useFixture(fixtures.MockPatch( + 'nova.virt.powervm.mgmt.mgmt_uuid', autospec=True)).mock + self.mgmt_uuid.return_value = 'mgmt_uuid' + + self.pvm_uuid = self.useFixture(fixtures.MockPatch( + 'nova.virt.powervm.vm.get_pvm_uuid')).mock + self.pvm_uuid.return_value = 'pvm_uuid' + + # Set up for the mocks for the disk adapter. + self.mock_find_vg = self.useFixture(fixtures.MockPatch( + 'pypowervm.tasks.storage.find_vg', autospec=True)).mock + self.vg_uuid = uuids.vg_uuid + self.vg = mock.Mock(spec=pvm_stg.VG, uuid=self.vg_uuid) + self.mock_find_vg.return_value = (self.vio_wrap, self.vg) + + self.flags(volume_group_name='fakevg', group='powervm') + + # Mock the feed tasks. + self.mock_afs = self.useFixture(fixtures.MockPatch( + 'pypowervm.utils.transaction.FeedTask.add_functor_subtask', + autospec=True)).mock + self.mock_wtsk = mock.create_autospec( + pvm_tx.WrapperTask, instance=True) + self.mock_wtsk.configure_mock(wrapper=self.vio_wrap) + self.mock_ftsk = mock.create_autospec(pvm_tx.FeedTask, instance=True) + self.mock_ftsk.configure_mock( + wrapper_tasks={'vios-uuid': self.mock_wtsk}) + + # Create the adapter. + self.ld_adpt = localdisk.LocalStorage(self.adpt, 'host_uuid') + + def test_init(self): + # Localdisk adapter already initialized in setUp() + # From super __init__() + self.assertEqual(self.adpt, self.ld_adpt._adapter) + self.assertEqual('host_uuid', self.ld_adpt._host_uuid) + self.assertEqual('mgmt_uuid', self.ld_adpt.mp_uuid) + + # From LocalStorage __init__() + self.assertEqual('fakevg', self.ld_adpt.vg_name) + self.mock_find_vg.assert_called_once_with(self.adpt, 'fakevg') + self.assertEqual('vios-uuid', self.ld_adpt._vios_uuid) + self.assertEqual(self.vg_uuid, self.ld_adpt.vg_uuid) + self.assertFalse(self.ld_adpt.capabilities['shared_storage']) + self.assertFalse(self.ld_adpt.capabilities['has_imagecache']) + self.assertFalse(self.ld_adpt.capabilities['snapshot']) + + # Assert snapshot capability is true if hosting I/O on mgmt partition. + self.mgmt_uuid.return_value = 'vios-uuid' + self.ld_adpt = localdisk.LocalStorage(self.adpt, 'host_uuid') + self.assertTrue(self.ld_adpt.capabilities['snapshot']) + + # Assert volume_group_name is required. + self.flags(volume_group_name=None, group='powervm') + self.assertRaises(exception.OptRequiredIfOtherOptValue, + localdisk.LocalStorage, self.adpt, 'host_uuid') + + def test_vios_uuids(self): + self.assertEqual(['vios-uuid'], self.ld_adpt._vios_uuids) + + @mock.patch('pypowervm.tasks.scsi_mapper.gen_match_func', autospec=True) + @mock.patch('nova.virt.powervm.disk.driver.DiskAdapter._get_disk_name') + def test_disk_match_func(self, mock_disk_name, mock_gen_match): + mock_disk_name.return_value = 'disk_name' + func = self.ld_adpt._disk_match_func('disk_type', 'instance') + mock_disk_name.assert_called_once_with( + 'disk_type', 'instance', short=True) + mock_gen_match.assert_called_once_with( + pvm_stg.VDisk, names=['disk_name']) + self.assertEqual(mock_gen_match.return_value, func) + + @mock.patch('nova.virt.powervm.disk.localdisk.LocalStorage._get_vg_wrap') + def test_capacity(self, mock_vg): + """Tests the capacity methods.""" + mock_vg.return_value = mock.Mock( + capacity='5120', available_size='2048') + self.assertEqual(5120.0, self.ld_adpt.capacity) + self.assertEqual(3072.0, self.ld_adpt.capacity_used) + + @mock.patch('pypowervm.tasks.storage.rm_vg_storage', autospec=True) + @mock.patch('nova.virt.powervm.disk.localdisk.LocalStorage._get_vg_wrap') + def test_delete_disks(self, mock_vg, mock_rm_vg): + self.ld_adpt.delete_disks('storage_elems') + mock_vg.assert_called_once_with() + mock_rm_vg.assert_called_once_with( + mock_vg.return_value, vdisks='storage_elems') + + @mock.patch('pypowervm.wrappers.virtual_io_server.VIOS.get') + @mock.patch('pypowervm.tasks.scsi_mapper.remove_maps', autospec=True) + @mock.patch('pypowervm.tasks.scsi_mapper.gen_match_func', autospec=True) + def test_detach_disk(self, mock_match_fn, mock_rm_maps, mock_vios): + mock_match_fn.return_value = 'match_func' + mock_vios.return_value = self.vio_wrap + mock_map1 = mock.Mock(backing_storage='back_stor1') + mock_map2 = mock.Mock(backing_storage='back_stor2') + mock_rm_maps.return_value = [mock_map1, mock_map2] + + back_stores = self.ld_adpt.detach_disk('instance') + + self.assertEqual(['back_stor1', 'back_stor2'], back_stores) + mock_match_fn.assert_called_once_with(pvm_stg.VDisk) + mock_vios.assert_called_once_with( + self.ld_adpt._adapter, uuid='vios-uuid', + xag=[pvm_const.XAG.VIO_SMAP]) + mock_rm_maps.assert_called_with(self.vio_wrap, 'pvm_uuid', + match_func=mock_match_fn.return_value) + mock_vios.return_value.update.assert_called_once() + + @mock.patch('pypowervm.tasks.scsi_mapper.remove_vdisk_mapping', + autospec=True) + def test_disconnect_disk_from_mgmt(self, mock_rm_vdisk_map): + self.ld_adpt.disconnect_disk_from_mgmt('vios-uuid', 'disk_name') + mock_rm_vdisk_map.assert_called_with( + self.ld_adpt._adapter, 'vios-uuid', 'mgmt_uuid', + disk_names=['disk_name']) + + @mock.patch('nova.virt.powervm.disk.localdisk.LocalStorage._upload_image') + def test_create_disk_from_image(self, mock_upload_image): + mock_image_meta = mock.Mock() + mock_image_meta.size = 30 + mock_upload_image.return_value = 'mock_img' + + self.ld_adpt.create_disk_from_image( + 'context', 'instance', mock_image_meta) + + mock_upload_image.assert_called_once_with( + 'context', 'instance', mock_image_meta) + + @mock.patch('nova.image.api.API.download') + @mock.patch('nova.virt.powervm.disk.driver.IterableToFileAdapter') + @mock.patch('pypowervm.tasks.storage.upload_new_vdisk') + @mock.patch('nova.virt.powervm.disk.driver.DiskAdapter._get_disk_name') + def test_upload_image(self, mock_name, mock_upload, mock_iter, mock_dl): + mock_meta = mock.Mock(id='1', size=1073741824, disk_format='raw') + mock_upload.return_value = ['mock_img'] + + mock_img = self.ld_adpt._upload_image('context', 'inst', mock_meta) + + self.assertEqual('mock_img', mock_img) + mock_name.assert_called_once_with( + disk_dvr.DiskType.BOOT, 'inst', short=True) + mock_dl.assert_called_once_with('context', '1') + mock_iter.assert_called_once_with(mock_dl.return_value) + mock_upload.assert_called_once_with( + self.adpt, 'vios-uuid', self.vg_uuid, mock_iter.return_value, + mock_name.return_value, 1073741824, d_size=1073741824, + upload_type=tsk_stg.UploadType.IO_STREAM, file_format='raw') + + @mock.patch('pypowervm.tasks.scsi_mapper.add_map', autospec=True) + @mock.patch('pypowervm.tasks.scsi_mapper.build_vscsi_mapping', + autospec=True) + def test_attach_disk(self, mock_bldmap, mock_addmap): + def test_afs(add_func): + # Verify the internal add_func + self.assertEqual(mock_addmap.return_value, add_func(self.vio_wrap)) + mock_bldmap.assert_called_once_with( + self.ld_adpt._host_uuid, self.vio_wrap, 'pvm_uuid', + 'disk_info') + mock_addmap.assert_called_once_with( + self.vio_wrap, mock_bldmap.return_value) + + self.mock_wtsk.add_functor_subtask.side_effect = test_afs + self.ld_adpt.attach_disk('instance', 'disk_info', self.mock_ftsk) + self.pvm_uuid.assert_called_once_with('instance') + self.assertEqual(1, self.mock_wtsk.add_functor_subtask.call_count) + + @mock.patch('pypowervm.wrappers.storage.VG.get') + def test_get_vg_wrap(self, mock_vg): + vg_wrap = self.ld_adpt._get_vg_wrap() + self.assertEqual(mock_vg.return_value, vg_wrap) + mock_vg.assert_called_once_with( + self.adpt, uuid=self.vg_uuid, parent_type=pvm_vios.VIOS, + parent_uuid='vios-uuid') + + @mock.patch('pypowervm.wrappers.virtual_io_server.VIOS.get') + @mock.patch('pypowervm.tasks.scsi_mapper.find_maps', autospec=True) + @mock.patch('nova.virt.powervm.disk.localdisk.LocalStorage.' + '_disk_match_func') + def test_get_bootdisk_path(self, mock_match_fn, mock_findmaps, + mock_vios): + mock_vios.return_value = self.vio_wrap + + # No maps found + mock_findmaps.return_value = None + devname = self.ld_adpt.get_bootdisk_path('inst', 'vios_uuid') + self.pvm_uuid.assert_called_once_with('inst') + mock_match_fn.assert_called_once_with(disk_dvr.DiskType.BOOT, 'inst') + mock_vios.assert_called_once_with( + self.adpt, uuid='vios_uuid', xag=[pvm_const.XAG.VIO_SMAP]) + mock_findmaps.assert_called_once_with( + self.vio_wrap.scsi_mappings, + client_lpar_id='pvm_uuid', + match_func=mock_match_fn.return_value) + self.assertIsNone(devname) + + # Good map + mock_lu = mock.Mock() + mock_lu.server_adapter.backing_dev_name = 'devname' + mock_findmaps.return_value = [mock_lu] + devname = self.ld_adpt.get_bootdisk_path('inst', 'vios_uuid') + self.assertEqual('devname', devname) + + @mock.patch('nova.virt.powervm.vm.get_instance_wrapper', autospec=True) + @mock.patch('pypowervm.tasks.scsi_mapper.find_maps') + @mock.patch('pypowervm.wrappers.virtual_io_server.VIOS.get') + @mock.patch('pypowervm.wrappers.storage.VG.get', new=mock.Mock()) + def test_get_bootdisk_iter(self, mock_vios, mock_find_maps, mock_lw): + inst, lpar_wrap, vios1 = self._bld_mocks_for_instance_disk() + mock_lw.return_value = lpar_wrap + + # Good path + mock_vios.return_value = vios1 + for vdisk, vios in self.ld_adpt._get_bootdisk_iter(inst): + self.assertEqual(vios1.scsi_mappings[0].backing_storage, vdisk) + self.assertEqual(vios1.uuid, vios.uuid) + mock_vios.assert_called_once_with( + self.adpt, uuid='vios-uuid', xag=[pvm_const.XAG.VIO_SMAP]) + + # Not found, no storage of that name. + mock_vios.reset_mock() + mock_find_maps.return_value = [] + for vdisk, vios in self.ld_adpt._get_bootdisk_iter(inst): + self.fail('Should not have found any storage elements.') + mock_vios.assert_called_once_with( + self.adpt, uuid='vios-uuid', xag=[pvm_const.XAG.VIO_SMAP]) + + @mock.patch('nova.virt.powervm.disk.driver.DiskAdapter._get_bootdisk_iter', + autospec=True) + @mock.patch('nova.virt.powervm.vm.get_instance_wrapper', autospec=True) + @mock.patch('pypowervm.tasks.scsi_mapper.add_vscsi_mapping', autospec=True) + def test_connect_instance_disk_to_mgmt(self, mock_add, mock_lw, mock_iter): + inst, lpar_wrap, vios1 = self._bld_mocks_for_instance_disk() + mock_lw.return_value = lpar_wrap + + # Good path + mock_iter.return_value = [(vios1.scsi_mappings[0].backing_storage, + vios1)] + vdisk, vios = self.ld_adpt.connect_instance_disk_to_mgmt(inst) + self.assertEqual(vios1.scsi_mappings[0].backing_storage, vdisk) + self.assertIs(vios1, vios) + self.assertEqual(1, mock_add.call_count) + mock_add.assert_called_with('host_uuid', vios, 'mgmt_uuid', vdisk) + + # add_vscsi_mapping raises. Show-stopper since only one VIOS. + mock_add.reset_mock() + mock_add.side_effect = Exception + self.assertRaises(exception.InstanceDiskMappingFailed, + self.ld_adpt.connect_instance_disk_to_mgmt, inst) + self.assertEqual(1, mock_add.call_count) + + # Not found + mock_add.reset_mock() + mock_iter.return_value = [] + self.assertRaises(exception.InstanceDiskMappingFailed, + self.ld_adpt.connect_instance_disk_to_mgmt, inst) + self.assertFalse(mock_add.called) + + def _bld_mocks_for_instance_disk(self): + inst = mock.Mock() + inst.name = 'Name Of Instance' + inst.uuid = uuids.inst_uuid + lpar_wrap = mock.Mock() + lpar_wrap.id = 2 + vios1 = self.vio_wrap + back_stor_name = 'b_Name_Of__' + inst.uuid[:4] + vios1.scsi_mappings[0].backing_storage.name = back_stor_name + return inst, lpar_wrap, vios1 diff --git a/nova/tests/unit/virt/powervm/test_driver.py b/nova/tests/unit/virt/powervm/test_driver.py index e3fe0c84ae33..b74f103d6799 100644 --- a/nova/tests/unit/virt/powervm/test_driver.py +++ b/nova/tests/unit/virt/powervm/test_driver.py @@ -73,10 +73,10 @@ class TestPowerVMDriver(test.NoDBTestCase): @mock.patch('nova.image.API') @mock.patch('pypowervm.tasks.storage.ComprehensiveScrub', autospec=True) - @mock.patch('nova.virt.powervm.disk.ssp.SSPDiskAdapter') + @mock.patch('oslo_utils.importutils.import_object_ns', autospec=True) @mock.patch('pypowervm.wrappers.managed_system.System', autospec=True) @mock.patch('pypowervm.tasks.partition.validate_vios_ready', autospec=True) - def test_init_host(self, mock_vvr, mock_sys, mock_ssp, mock_scrub, + def test_init_host(self, mock_vvr, mock_sys, mock_import, mock_scrub, mock_img): mock_hostw = mock.Mock(uuid='uuid') mock_sys.get.return_value = [mock_hostw] @@ -90,8 +90,10 @@ class TestPowerVMDriver(test.NoDBTestCase): self.assertEqual(mock_hostw, self.drv.host_wrapper) mock_scrub.assert_called_once_with(self.drv.adapter) mock_scrub.return_value.execute.assert_called_once_with() - mock_ssp.assert_called_once_with(self.drv.adapter, 'uuid') - self.assertEqual(mock_ssp.return_value, self.drv.disk_dvr) + mock_import.assert_called_once_with( + 'nova.virt.powervm.disk', 'localdisk.LocalStorage', + self.drv.adapter, 'uuid') + self.assertEqual(mock_import.return_value, self.drv.disk_dvr) mock_img.assert_called_once_with() self.assertEqual(mock_img.return_value, self.drv.image_api) @@ -309,6 +311,10 @@ class TestPowerVMDriver(test.NoDBTestCase): mock_rm.assert_called_once_with( stg_elem='stg_elem', vios_wrap='vios_wrap', disk_path='disk_path') + self.drv.disk_dvr.capabilities = {'snapshot': False} + self.assertRaises(exception.NotSupportedWithOption, self.drv.snapshot, + 'context', self.inst, 'image_id', 'update_task_state') + def test_power_on(self): self.drv.power_on('context', self.inst, 'network_info') self.pwron.assert_called_once_with(self.adp, self.inst) diff --git a/nova/virt/powervm/disk/localdisk.py b/nova/virt/powervm/disk/localdisk.py new file mode 100644 index 000000000000..7baa674c7279 --- /dev/null +++ b/nova/virt/powervm/disk/localdisk.py @@ -0,0 +1,211 @@ +# Copyright 2013 OpenStack Foundation +# Copyright 2015, 2018 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 oslo_log.log as logging +from pypowervm import const as pvm_const +from pypowervm.tasks import scsi_mapper as tsk_map +from pypowervm.tasks import storage as tsk_stg +from pypowervm.wrappers import storage as pvm_stg +from pypowervm.wrappers import virtual_io_server as pvm_vios + +from nova import conf +from nova import exception +from nova import image +from nova.virt.powervm.disk import driver as disk_dvr +from nova.virt.powervm import vm + +LOG = logging.getLogger(__name__) +CONF = conf.CONF +IMAGE_API = image.API() + + +class LocalStorage(disk_dvr.DiskAdapter): + + def __init__(self, adapter, host_uuid): + super(LocalStorage, self).__init__(adapter, host_uuid) + + self.capabilities = { + 'shared_storage': False, + 'has_imagecache': False, + # NOTE(efried): 'snapshot' capability set dynamically below. + } + + # Query to get the Volume Group UUID + if not CONF.powervm.volume_group_name: + raise exception.OptRequiredIfOtherOptValue( + if_opt='disk_driver', if_value='localdisk', + then_opt='volume_group_name') + self.vg_name = CONF.powervm.volume_group_name + vios_w, vg_w = tsk_stg.find_vg(adapter, self.vg_name) + self._vios_uuid = vios_w.uuid + self.vg_uuid = vg_w.uuid + # Set the 'snapshot' capability dynamically. If we're hosting I/O on + # the management partition, we can snapshot. If we're hosting I/O on + # traditional VIOS, we are limited by the fact that a VSCSI device + # can't be mapped to two partitions (the VIOS and the management) at + # once. + self.capabilities['snapshot'] = self.mp_uuid == self._vios_uuid + LOG.info("Local Storage driver initialized: volume group: '%s'", + self.vg_name) + + @property + def _vios_uuids(self): + """List the UUIDs of the Virtual I/O Servers hosting the storage. + + For localdisk, there's only one. + """ + return [self._vios_uuid] + + @staticmethod + def _disk_match_func(disk_type, instance): + """Return a matching function to locate the disk for an instance. + + :param disk_type: One of the DiskType enum values. + :param instance: The instance whose disk is to be found. + :return: Callable suitable for the match_func parameter of the + pypowervm.tasks.scsi_mapper.find_maps method. + """ + disk_name = LocalStorage._get_disk_name( + disk_type, instance, short=True) + return tsk_map.gen_match_func(pvm_stg.VDisk, names=[disk_name]) + + @property + def capacity(self): + """Capacity of the storage in gigabytes.""" + vg_wrap = self._get_vg_wrap() + return float(vg_wrap.capacity) + + @property + def capacity_used(self): + """Capacity of the storage in gigabytes that is used.""" + vg_wrap = self._get_vg_wrap() + # Subtract available from capacity + return float(vg_wrap.capacity) - float(vg_wrap.available_size) + + def delete_disks(self, storage_elems): + """Removes the specified disks. + + :param storage_elems: A list of the storage elements that are to be + deleted. Derived from the return value from + detach_disk. + """ + # All of localdisk is done against the volume group. So reload + # that (to get new etag) and then update against it. + tsk_stg.rm_vg_storage(self._get_vg_wrap(), vdisks=storage_elems) + + def detach_disk(self, instance): + """Detaches the storage adapters from the image disk. + + :param instance: Instance to disconnect the image for. + :return: A list of all the backing storage elements that were + disconnected from the I/O Server and VM. + """ + lpar_uuid = vm.get_pvm_uuid(instance) + + # Build the match function + match_func = tsk_map.gen_match_func(pvm_stg.VDisk) + + vios_w = pvm_vios.VIOS.get( + self._adapter, uuid=self._vios_uuid, xag=[pvm_const.XAG.VIO_SMAP]) + + # Remove the mappings. + mappings = tsk_map.remove_maps( + vios_w, lpar_uuid, match_func=match_func) + + # Update the VIOS with the removed mappings. + vios_w.update() + + return [x.backing_storage for x in mappings] + + def disconnect_disk_from_mgmt(self, vios_uuid, disk_name): + """Disconnect a disk from the management partition. + + :param vios_uuid: The UUID of the Virtual I/O Server serving the + mapping. + :param disk_name: The name of the disk to unmap. + """ + tsk_map.remove_vdisk_mapping(self._adapter, vios_uuid, self.mp_uuid, + disk_names=[disk_name]) + LOG.info("Unmapped boot disk %(disk_name)s from the management " + "partition from Virtual I/O Server %(vios_name)s.", + {'disk_name': disk_name, 'mp_uuid': self.mp_uuid, + 'vios_name': vios_uuid}) + + def create_disk_from_image(self, context, instance, image_meta): + """Creates a disk and copies the specified image to it. + + Cleans up the created disk if an error occurs. + + :param context: nova context used to retrieve image from glance + :param instance: instance to create the disk for. + :param image_meta: The metadata of the image of the instance. + :return: The backing pypowervm storage object that was created. + """ + LOG.info('Create disk.', instance=instance) + + return self._upload_image(context, instance, image_meta) + + # TODO(esberglu): Copy vdisk when implementing image cache. + + def _upload_image(self, context, instance, image_meta): + """Upload a new image. + + :param context: Nova context used to retrieve image from glance. + :param image_meta: The metadata of the image of the instance. + :return: The virtual disk containing the image. + """ + + img_name = self._get_disk_name(disk_dvr.DiskType.BOOT, instance, + short=True) + + # TODO(esberglu) Add check for cached image when adding imagecache. + + return tsk_stg.upload_new_vdisk( + self._adapter, self._vios_uuid, self.vg_uuid, + disk_dvr.IterableToFileAdapter( + IMAGE_API.download(context, image_meta.id)), img_name, + image_meta.size, d_size=image_meta.size, + upload_type=tsk_stg.UploadType.IO_STREAM, + file_format=image_meta.disk_format)[0] + + def attach_disk(self, instance, disk_info, stg_ftsk): + """Attaches the disk image to the Virtual Machine. + + :param instance: nova instance to connect the disk to. + :param disk_info: The pypowervm storage element returned from + create_disk_from_image. Ex. VOptMedia, VDisk, LU, + or PV. + :param stg_ftsk: The pypowervm transaction FeedTask for the + I/O Operations. The Virtual I/O Server mapping updates + will be added to the FeedTask. This defers the updates + to some later point in time. + """ + lpar_uuid = vm.get_pvm_uuid(instance) + + def add_func(vios_w): + LOG.info("Adding logical volume disk connection to VIOS %(vios)s.", + {'vios': vios_w.name}, instance=instance) + mapping = tsk_map.build_vscsi_mapping( + self._host_uuid, vios_w, lpar_uuid, disk_info) + return tsk_map.add_map(vios_w, mapping) + + stg_ftsk.wrapper_tasks[self._vios_uuid].add_functor_subtask(add_func) + + def _get_vg_wrap(self): + return pvm_stg.VG.get(self._adapter, uuid=self.vg_uuid, + parent_type=pvm_vios.VIOS, + parent_uuid=self._vios_uuid) diff --git a/nova/virt/powervm/driver.py b/nova/virt/powervm/driver.py index 7aa247ad3b44..e2633ee3c86d 100644 --- a/nova/virt/powervm/driver.py +++ b/nova/virt/powervm/driver.py @@ -15,6 +15,7 @@ from oslo_log import log as logging from oslo_utils import excutils +from oslo_utils import importutils from pypowervm import adapter as pvm_apt from pypowervm import const as pvm_const from pypowervm import exceptions as pvm_exc @@ -35,7 +36,6 @@ from nova.i18n import _ from nova import image from nova.virt import configdrive from nova.virt import driver -from nova.virt.powervm.disk import ssp from nova.virt.powervm import host as pvm_host from nova.virt.powervm.tasks import base as tf_base from nova.virt.powervm.tasks import image as tf_img @@ -47,6 +47,12 @@ from nova.virt.powervm import vm LOG = logging.getLogger(__name__) CONF = cfg.CONF +DISK_ADPT_NS = 'nova.virt.powervm.disk' +DISK_ADPT_MAPPINGS = { + 'localdisk': 'localdisk.LocalStorage', + 'ssp': 'ssp.SSPDiskAdapter' +} + class PowerVMDriver(driver.ComputeDriver): """PowerVM NovaLink Implementation of Compute Driver. @@ -91,9 +97,9 @@ class PowerVMDriver(driver.ComputeDriver): pvm_stor.ComprehensiveScrub(self.adapter).execute() # Initialize the disk adapter - # TODO(efried): Other disk adapters (localdisk), by conf selection. - self.disk_dvr = ssp.SSPDiskAdapter(self.adapter, - self.host_wrapper.uuid) + self.disk_dvr = importutils.import_object_ns( + DISK_ADPT_NS, DISK_ADPT_MAPPINGS[CONF.powervm.disk_driver.lower()], + self.adapter, self.host_wrapper.uuid) self.image_api = image.API() LOG.info("The PowerVM compute driver has been initialized.") @@ -312,8 +318,13 @@ class PowerVMDriver(driver.ComputeDriver): nova.compute.task_states.IMAGE_SNAPSHOT. See nova.objects.instance.Instance.save for expected_task_state usage. """ - # TODO(esberglu) Add check for disk driver snapshot capability when - # additional disk drivers are implemented. + + if not self.disk_dvr.capabilities.get('snapshot'): + raise exc.NotSupportedWithOption( + message=_("The snapshot operation is not supported in " + "conjunction with a [powervm]/disk_driver setting " + "of %s.") % CONF.powervm.disk_driver) + self._log_operation('snapshot', instance) # Define the flow. diff --git a/releasenotes/notes/powervm-localdisk-ccdf2347226303a8.yaml b/releasenotes/notes/powervm-localdisk-ccdf2347226303a8.yaml new file mode 100644 index 000000000000..e0d63e5f0c6a --- /dev/null +++ b/releasenotes/notes/powervm-localdisk-ccdf2347226303a8.yaml @@ -0,0 +1,15 @@ +--- +features: + - | + The PowerVM virt driver now supports booting from local ephemeral disk. + Two new configuration options have been introduced to the ``powervm`` + configuration group, ``disk_driver`` and ``volume_group_name``. The former + allows the selection of either ssp or localdisk for the PowerVM disk + driver. The latter specifies the name of the volume group when using the + localdisk disk driver. +upgrade: + - | + The PowerVM virt driver previously used the PowerVM Shared Storage Pool + disk driver by default. The default disk driver for PowerVM is now + localdisk. See configuration option ``[powervm]/disk_driver`` for usage + details.