From ebee3eae88a214ecf340e36b69dc8af8a609f6e9 Mon Sep 17 00:00:00 2001 From: Drew Thorstensen Date: Wed, 18 Jan 2017 17:04:11 -0500 Subject: [PATCH] Experimental: File I/O Volume Driver Implementation Implements the volume driver that supports file backed cinder volumes. Resolves long standing issue with how our volume adapters are created and now allows for a single host to support multiple different volume types at once. This is considered experimental. Full support should land in Pike. To use, the NovaLink nightly builds must be utilized. Change-Id: Ia78d2ed17232cc0f42fed1391786384e4dea28c1 Partially-Implements: bp/file-io-cinder-connector --- nova/virt/powervm_ext/driver.py | 3 +- nova_powervm/conf/powervm.py | 2 +- .../tests/virt/powervm/test_driver.py | 53 ++++----- .../tests/virt/powervm/volume/test_driver.py | 70 ++++++++++++ .../tests/virt/powervm/volume/test_fileio.py | 99 +++++++++++++++++ .../tests/virt/powervm/volume/test_gpfs.py | 40 +++++++ .../tests/virt/powervm/volume/test_local.py | 40 +++++++ .../tests/virt/powervm/volume/test_nfs.py | 40 +++++++ nova_powervm/virt/powervm/driver.py | 101 +++++------------- nova_powervm/virt/powervm/volume/__init__.py | 57 +++++++++- nova_powervm/virt/powervm/volume/driver.py | 7 -- nova_powervm/virt/powervm/volume/fileio.py | 97 +++++++++++++++++ nova_powervm/virt/powervm/volume/gpfs.py | 24 +++++ nova_powervm/virt/powervm/volume/iscsi.py | 3 - nova_powervm/virt/powervm/volume/local.py | 24 +++++ nova_powervm/virt/powervm/volume/nfs.py | 26 +++++ nova_powervm/virt/powervm/volume/npiv.py | 8 -- nova_powervm/virt/powervm/volume/vscsi.py | 7 -- 18 files changed, 561 insertions(+), 140 deletions(-) create mode 100644 nova_powervm/tests/virt/powervm/volume/test_fileio.py create mode 100644 nova_powervm/tests/virt/powervm/volume/test_gpfs.py create mode 100644 nova_powervm/tests/virt/powervm/volume/test_local.py create mode 100644 nova_powervm/tests/virt/powervm/volume/test_nfs.py create mode 100644 nova_powervm/virt/powervm/volume/fileio.py create mode 100644 nova_powervm/virt/powervm/volume/gpfs.py create mode 100644 nova_powervm/virt/powervm/volume/local.py create mode 100644 nova_powervm/virt/powervm/volume/nfs.py diff --git a/nova/virt/powervm_ext/driver.py b/nova/virt/powervm_ext/driver.py index 0ddca7c9..1c9ec179 100644 --- a/nova/virt/powervm_ext/driver.py +++ b/nova/virt/powervm_ext/driver.py @@ -1,4 +1,4 @@ -# Copyright 2016 IBM Corp. +# Copyright 2016, 2017 IBM Corp. # # All Rights Reserved. # @@ -24,7 +24,6 @@ import nova_powervm.virt.powervm.driver as real_drv LOG = real_drv.LOG CONF = real_drv.CONF -VOLUME_DRIVER_MAPPINGS = real_drv.VOLUME_DRIVER_MAPPINGS DISK_ADPT_NS = real_drv.DISK_ADPT_NS DISK_ADPT_MAPPINGS = real_drv.DISK_ADPT_MAPPINGS NVRAM_NS = real_drv.NVRAM_NS diff --git a/nova_powervm/conf/powervm.py b/nova_powervm/conf/powervm.py index 75f1f8b9..490c4380 100644 --- a/nova_powervm/conf/powervm.py +++ b/nova_powervm/conf/powervm.py @@ -116,7 +116,7 @@ ssp_opts = [ vol_adapter_opts = [ cfg.StrOpt('fc_attach_strategy', choices=['vscsi', 'npiv'], ignore_case=True, - default='vscsi', + default='vscsi', mutable=True, help='The Fibre Channel Volume Strategy defines how FC Cinder ' 'volumes should be attached to the Virtual Machine. The ' 'options are: npiv or vscsi. If npiv is selected then ' diff --git a/nova_powervm/tests/virt/powervm/test_driver.py b/nova_powervm/tests/virt/powervm/test_driver.py index d81f64b1..9c0c844e 100644 --- a/nova_powervm/tests/virt/powervm/test_driver.py +++ b/nova_powervm/tests/virt/powervm/test_driver.py @@ -1,4 +1,4 @@ -# Copyright 2014, 2016 IBM Corp. +# Copyright 2014, 2017 IBM Corp. # # All Rights Reserved. # @@ -13,7 +13,6 @@ # 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 __future__ import absolute_import @@ -51,7 +50,6 @@ from nova_powervm.tests.virt.powervm import fixtures as fx from nova_powervm.virt.powervm import exception as p_exc from nova_powervm.virt.powervm import live_migration as lpm from nova_powervm.virt.powervm import vm -from nova_powervm.virt.powervm import volume as vol_attach LOG = logging.getLogger(__name__) logging.basicConfig() @@ -162,15 +160,6 @@ class TestPowerVMDriver(test.TestCase): self.lpm_inst.uuid = 'inst1' self.drv.live_migrations = {'inst1': self.lpm} - def _vol_drv_maps(self): - VOLUME_DRIVER_MAPPINGS = { - 'fibre_channel': vol_attach.FC_STRATEGY_MAPPING[ - driver.CONF.powervm.fc_attach_strategy.lower()], - 'iscsi': vol_attach.NETWORK_STRATEGY_MAPPING[ - driver.CONF.powervm.network_attach_strategy.lower()], - } - return VOLUME_DRIVER_MAPPINGS - def test_driver_create(self): """Validates that a driver of the PowerVM type can be initialized.""" test_drv = driver.PowerVMDriver(fake.FakeVirtAPI()) @@ -863,44 +852,40 @@ class TestPowerVMDriver(test.TestCase): ret = self.drv._is_booted_from_volume(None) self.assertFalse(ret) - @mock.patch('nova_powervm.virt.powervm.driver.VOLUME_DRIVER_MAPPINGS') - def test_get_inst_xag(self, mock_mapping): - def getitem(name): - return self._vol_drv_maps()[name] - mock_mapping.__getitem__.side_effect = getitem - - self.flags(volume_adapter='fibre_channel', group='powervm') + def test_get_inst_xag(self): # No volumes - should be just the SCSI mapping xag = self.drv._get_inst_xag(mock.Mock(), None) self.assertEqual([pvm_const.XAG.VIO_SMAP], xag) # The vSCSI Volume attach - only needs the SCSI mapping. self.flags(fc_attach_strategy='vscsi', group='powervm') - xag = self.drv._get_inst_xag(mock.Mock(), [mock.Mock()]) + mock_bdm = {'connection_info': + {'driver_volume_type': 'fibre_channel'}} + xag = self.drv._get_inst_xag(mock.Mock(), [mock_bdm]) self.assertEqual([pvm_const.XAG.VIO_SMAP], xag) # The NPIV volume attach - requires SCSI, Storage and FC Mapping self.flags(fc_attach_strategy='npiv', group='powervm') - mock_mapping.return_value = self._vol_drv_maps() - xag = self.drv._get_inst_xag(mock.Mock(), [mock.Mock()]) - self.assertEqual({pvm_const.XAG.VIO_STOR, - pvm_const.XAG.VIO_SMAP, + xag = self.drv._get_inst_xag(mock.Mock(), [mock_bdm]) + self.assertEqual({pvm_const.XAG.VIO_STOR, pvm_const.XAG.VIO_SMAP, pvm_const.XAG.VIO_FMAP}, set(xag)) # The vSCSI Volume attach - Ensure case insensitive. self.flags(fc_attach_strategy='VSCSI', group='powervm') - mock_mapping.return_value = self._vol_drv_maps() - xag = self.drv._get_inst_xag(mock.Mock(), [mock.Mock()]) + xag = self.drv._get_inst_xag(mock.Mock(), [mock_bdm]) self.assertEqual([pvm_const.XAG.VIO_SMAP], xag) - # The iSCSI volume attach - only nees the SCSI mapping. - self.flags(volume_adapter='iscsi', group='powervm') - mock_mapping.return_value = self._vol_drv_maps() - xag = self.drv._get_inst_xag(mock.Mock(), [mock.Mock()]) - self.assertEqual([pvm_const.XAG.VIO_SMAP], xag) + # Validate the other volume types only return SCSI mappings + vol_types = ['iscsi', 'gpfs', 'local', 'nfs'] + for vol_type in vol_types: + self.flags(volume_adapter='iscsi', group='powervm') + mock_bdm = {'connection_info': + {'driver_volume_type': vol_type}} + xag = self.drv._get_inst_xag(mock.Mock(), [mock_bdm]) + self.assertEqual([pvm_const.XAG.VIO_SMAP], xag) # If a recreate, all should be returned - xag = self.drv._get_inst_xag(mock.Mock(), [mock.Mock()], recreate=True) + xag = self.drv._get_inst_xag(mock.Mock(), [mock_bdm], recreate=True) self.assertEqual({pvm_const.XAG.VIO_STOR, pvm_const.XAG.VIO_SMAP, pvm_const.XAG.VIO_FMAP}, set(xag)) @@ -2041,8 +2026,8 @@ class TestPowerVMDriver(test.TestCase): def _get_results(block_device_info=None, bdms=None): # Patch so we get the same mock back each time. - with mock.patch.object(self.drv, '_get_inst_vol_adpt', - return_value=vol_adpt): + with mock.patch('nova_powervm.virt.powervm.volume.' + 'build_volume_driver', return_value=vol_adpt): return [ (bdm, vol_drv) for bdm, vol_drv in self.drv._vol_drv_iter( 'context', self.inst, diff --git a/nova_powervm/tests/virt/powervm/volume/test_driver.py b/nova_powervm/tests/virt/powervm/volume/test_driver.py index 01a23bed..bc9cba20 100644 --- a/nova_powervm/tests/virt/powervm/volume/test_driver.py +++ b/nova_powervm/tests/virt/powervm/volume/test_driver.py @@ -16,10 +16,17 @@ import mock from pypowervm.wrappers import virtual_io_server as pvm_vios +import six from nova import test from nova_powervm.virt.powervm import volume +from nova_powervm.virt.powervm.volume import gpfs +from nova_powervm.virt.powervm.volume import iscsi +from nova_powervm.virt.powervm.volume import local +from nova_powervm.virt.powervm.volume import nfs +from nova_powervm.virt.powervm.volume import npiv +from nova_powervm.virt.powervm.volume import vscsi class TestVolumeAdapter(test.TestCase): @@ -39,6 +46,14 @@ class TestVolumeAdapter(test.TestCase): class TestInitMethods(test.TestCase): + # Volume driver types to classes + volume_drivers = { + 'iscsi': iscsi.IscsiVolumeAdapter, + 'local': local.LocalVolumeAdapter, + 'nfs': nfs.NFSVolumeAdapter, + 'gpfs': gpfs.GPFSVolumeAdapter, + } + @mock.patch('pypowervm.tasks.hdisk.discover_iscsi_initiator') @mock.patch('pypowervm.tasks.partition.get_mgmt_partition') def test_get_iscsi_initiator(self, mock_mgmt, mock_iscsi_init): @@ -61,3 +76,58 @@ class TestInitMethods(test.TestCase): self.assertEqual('test_initiator', volume.get_iscsi_initiator(mock_adpt)) self.assertEqual(1, mock_mgmt.call_count) + + def test_get_volume_class(self): + for vol_type, class_type in six.iteritems(self.volume_drivers): + self.assertEqual(class_type, volume.get_volume_class(vol_type)) + + # Try the fibre as vscsi + self.flags(fc_attach_strategy='vscsi', group='powervm') + self.assertEqual(vscsi.PVVscsiFCVolumeAdapter, + volume.get_volume_class('fibre_channel')) + + # Try the fibre as npiv + self.flags(fc_attach_strategy='npiv', group='powervm') + self.assertEqual(npiv.NPIVVolumeAdapter, + volume.get_volume_class('fibre_channel')) + + def test_build_volume_driver(self): + for vol_type, class_type in six.iteritems(self.volume_drivers): + vdrv = volume.build_volume_driver( + mock.Mock(), "abc123", mock.Mock(uuid='abc1'), + {'driver_volume_type': vol_type}) + self.assertIsInstance(vdrv, class_type) + + # Try the fibre as vscsi + self.flags(fc_attach_strategy='vscsi', group='powervm') + vdrv = volume.build_volume_driver( + mock.Mock(), "abc123", mock.Mock(uuid='abc1'), + {'driver_volume_type': 'fibre_channel'}) + self.assertIsInstance(vdrv, vscsi.PVVscsiFCVolumeAdapter) + + # Try the fibre as npiv + self.flags(fc_attach_strategy='npiv', group='powervm') + vdrv = volume.build_volume_driver( + mock.Mock(), "abc123", mock.Mock(uuid='abc1'), + {'driver_volume_type': 'fibre_channel'}) + self.assertIsInstance(vdrv, npiv.NPIVVolumeAdapter) + + def test_hostname_for_volume(self): + self.flags(host='test_host') + mock_instance = mock.Mock() + mock_instance.name = 'instance' + + # Try the fibre as vscsi + self.flags(fc_attach_strategy='vscsi', group='powervm') + self.assertEqual("test_host", + volume.get_hostname_for_volume(mock_instance)) + + # Try the fibre as npiv + self.flags(fc_attach_strategy='npiv', group='powervm') + self.assertEqual("test_host_instance", + volume.get_hostname_for_volume(mock_instance)) + + # NPIV with long host name + self.flags(host='really_long_host_name_too_long') + self.assertEqual("really_long_host_nam_instance", + volume.get_hostname_for_volume(mock_instance)) diff --git a/nova_powervm/tests/virt/powervm/volume/test_fileio.py b/nova_powervm/tests/virt/powervm/volume/test_fileio.py new file mode 100644 index 00000000..f66552b2 --- /dev/null +++ b/nova_powervm/tests/virt/powervm/volume/test_fileio.py @@ -0,0 +1,99 @@ +# Copyright 2017 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 mock +from pypowervm import const as pvm_const +from pypowervm.tests import test_fixtures as pvm_fx +from pypowervm.wrappers import storage as pvm_stg + +from nova_powervm.tests.virt.powervm.volume import test_driver as test_vol +from nova_powervm.virt.powervm.volume import fileio as v_drv + + +class FakeFileIOVolAdapter(v_drv.FileIOVolumeAdapter): + """Subclass for FileIOVolumeAdapter, since it is abstract.""" + + def __init__(self, adapter, host_uuid, instance, connection_info, + stg_ftsk=None): + super(FakeFileIOVolAdapter, self).__init__( + adapter, host_uuid, instance, connection_info, stg_ftsk=stg_ftsk) + + def _get_path(self): + return "fake_path" + + +class TestFileIOVolumeAdapter(test_vol.TestVolumeAdapter): + """Tests the FileIOVolumeAdapter. NovaLink is a I/O host.""" + + def setUp(self): + super(TestFileIOVolumeAdapter, self).setUp() + + # Needed for the volume adapter + self.adpt = self.useFixture(pvm_fx.AdapterFx()).adpt + mock_inst = mock.MagicMock(uuid='2BC123') + + self.vol_drv = FakeFileIOVolAdapter(self.adpt, 'host_uuid', mock_inst, + None) + + self.mock_vio_task = mock.MagicMock() + self.mock_stg_ftsk = mock.MagicMock( + wrapper_tasks={'vios_uuid': self.mock_vio_task}) + self.vol_drv.stg_ftsk = self.mock_stg_ftsk + + def test_min_xags(self): + """Ensures xag's only returns SCSI Mappings.""" + self.assertEqual([pvm_const.XAG.VIO_SMAP], self.vol_drv.min_xags()) + + @mock.patch('pypowervm.tasks.partition.get_mgmt_partition') + @mock.patch('pypowervm.wrappers.storage.FileIO.bld') + def test_connect_volume(self, mock_file_bld, mock_get_mgmt_partition): + # Mockups + mock_file = mock.Mock() + mock_file_bld.return_value = mock_file + mock_slot_mgr = mock.MagicMock() + + mock_vios = mock.Mock(uuid='vios_uuid') + mock_get_mgmt_partition.return_value = mock_vios + + # Invoke + self.vol_drv._connect_volume(mock_slot_mgr) + + # Validate + mock_file_bld.assert_called_once_with(self.adpt, 'fake_path') + + @mock.patch('pypowervm.tasks.partition.get_mgmt_partition') + @mock.patch('pypowervm.tasks.scsi_mapper.gen_match_func') + @mock.patch('pypowervm.tasks.scsi_mapper.find_maps') + def test_disconnect_volume(self, mock_find_maps, mock_gen_match_func, + mock_get_mgmt_partition): + # Mockups + mock_slot_mgr = mock.MagicMock() + + mock_vios = mock.Mock(uuid='vios_uuid') + mock_get_mgmt_partition.return_value = mock_vios + + mock_match_func = mock.Mock() + mock_gen_match_func.return_value = mock_match_func + + # Invoke + self.vol_drv._disconnect_volume(mock_slot_mgr) + + # Validate + mock_gen_match_func.assert_called_once_with( + pvm_stg.FileIO, names=['fake_path']) + mock_find_maps.assert_called_once_with( + mock_vios.scsi_mappings, client_lpar_id='2BC123', + match_func=mock_match_func) diff --git a/nova_powervm/tests/virt/powervm/volume/test_gpfs.py b/nova_powervm/tests/virt/powervm/volume/test_gpfs.py new file mode 100644 index 00000000..3348f28f --- /dev/null +++ b/nova_powervm/tests/virt/powervm/volume/test_gpfs.py @@ -0,0 +1,40 @@ +# Copyright 2017 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 mock + +from nova_powervm.tests.virt.powervm.volume import test_driver as test_vol +from nova_powervm.virt.powervm.volume import gpfs as v_drv + + +class TestGPFSVolumeAdapter(test_vol.TestVolumeAdapter): + """Tests the GPFSVolumeAdapter. NovaLink is a I/O host.""" + + def setUp(self): + super(TestGPFSVolumeAdapter, self).setUp() + + # Needed for the volume adapter + self.adpt = mock.Mock() + mock_inst = mock.MagicMock(uuid='2BC123') + + # Connection Info + mock_conn_info = {'data': {'device_path': '/gpfs/path'}} + + self.vol_drv = v_drv.GPFSVolumeAdapter( + self.adpt, 'host_uuid', mock_inst, mock_conn_info) + + def test_get_path(self): + self.assertEqual('/gpfs/path', self.vol_drv._get_path()) diff --git a/nova_powervm/tests/virt/powervm/volume/test_local.py b/nova_powervm/tests/virt/powervm/volume/test_local.py new file mode 100644 index 00000000..5b2eada5 --- /dev/null +++ b/nova_powervm/tests/virt/powervm/volume/test_local.py @@ -0,0 +1,40 @@ +# Copyright 2017 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 mock + +from nova_powervm.tests.virt.powervm.volume import test_driver as test_vol +from nova_powervm.virt.powervm.volume import local as v_drv + + +class TestLocalVolumeAdapter(test_vol.TestVolumeAdapter): + """Tests the LocalVolumeAdapter. NovaLink is a I/O host.""" + + def setUp(self): + super(TestLocalVolumeAdapter, self).setUp() + + # Needed for the volume adapter + self.adpt = mock.Mock() + mock_inst = mock.MagicMock(uuid='2BC123') + + # Connection Info + mock_conn_info = {'data': {'device_path': '/local/path'}} + + self.vol_drv = v_drv.LocalVolumeAdapter( + self.adpt, 'host_uuid', mock_inst, mock_conn_info) + + def test_get_path(self): + self.assertEqual('/local/path', self.vol_drv._get_path()) diff --git a/nova_powervm/tests/virt/powervm/volume/test_nfs.py b/nova_powervm/tests/virt/powervm/volume/test_nfs.py new file mode 100644 index 00000000..38ec69cd --- /dev/null +++ b/nova_powervm/tests/virt/powervm/volume/test_nfs.py @@ -0,0 +1,40 @@ +# Copyright 2017 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 mock + +from nova_powervm.tests.virt.powervm.volume import test_driver as test_vol +from nova_powervm.virt.powervm.volume import nfs as v_drv + + +class TestNFSVolumeAdapter(test_vol.TestVolumeAdapter): + """Tests the NFSVolumeAdapter. NovaLink is a I/O host.""" + + def setUp(self): + super(TestNFSVolumeAdapter, self).setUp() + + # Needed for the volume adapter + self.adpt = mock.Mock() + mock_inst = mock.MagicMock(uuid='2BC123') + + # Connection Info + mock_conn_info = {'data': {'export': '/nfs', 'name': 'path'}} + + self.vol_drv = v_drv.NFSVolumeAdapter( + self.adpt, 'host_uuid', mock_inst, mock_conn_info) + + def test_get_path(self): + self.assertEqual('/nfs/path', self.vol_drv._get_path()) diff --git a/nova_powervm/virt/powervm/driver.py b/nova_powervm/virt/powervm/driver.py index 4769d7c8..1885d4e9 100644 --- a/nova_powervm/virt/powervm/driver.py +++ b/nova_powervm/virt/powervm/driver.py @@ -1,4 +1,4 @@ -# Copyright 2014, 2016 IBM Corp. +# Copyright 2014, 2017 IBM Corp. # # All Rights Reserved. # @@ -73,16 +73,6 @@ from nova_powervm.virt.powervm import volume as vol_attach LOG = logging.getLogger(__name__) CONF = cfg.CONF -# Defines, for all cinder volume types, which volume driver to use. Currently -# only supports Fibre Channel, which has multiple options for connections, and -# iSCSI. -VOLUME_DRIVER_MAPPINGS = { - 'fibre_channel': vol_attach.FC_STRATEGY_MAPPING[ - CONF.powervm.fc_attach_strategy.lower()], - 'iscsi': vol_attach.NETWORK_STRATEGY_MAPPING[ - CONF.powervm.network_attach_strategy.lower()], -} - DISK_ADPT_NS = 'nova_powervm.virt.powervm.disk' DISK_ADPT_MAPPINGS = { 'localdisk': 'localdisk.LocalStorage', @@ -752,8 +742,8 @@ class PowerVMDriver(driver.ComputeDriver): # Determine if there are volumes to connect. If so, add a connection # for each type. slot_mgr = slot.build_slot_mgr(instance, self.store_api) - vol_drv = self._get_inst_vol_adpt(context, instance, - conn_info=connection_info) + vol_drv = vol_attach.build_volume_driver( + self.adapter, self.host_uuid, instance, connection_info) flow.add(tf_stg.ConnectVolume(vol_drv, slot_mgr)) # Save the new slot info @@ -776,8 +766,8 @@ class PowerVMDriver(driver.ComputeDriver): self._log_operation('detach_volume', instance) # Get a volume adapter for this volume - vol_drv = self._get_inst_vol_adpt(ctx.get_admin_context(), instance, - conn_info=connection_info) + vol_drv = vol_attach.build_volume_driver( + self.adapter, self.host_uuid, instance, connection_info) # Before attempting to detach a volume, ensure the instance exists # If a live migration fails, the compute manager will call detach @@ -1129,22 +1119,14 @@ class PowerVMDriver(driver.ComputeDriver): } """ - # The host ID - connector = {'host': CONF.host} - - # Get the contents from the volume driver - vol_drv = self._get_inst_vol_adpt(ctx.get_admin_context(), - instance) - if vol_drv is not None: - - if CONF.powervm.volume_adapter.lower() == "fibre_channel": - # Set the WWPNs - wwpn_list = vol_drv.wwpns() - if wwpn_list is not None: - connector["wwpns"] = wwpn_list - connector['host'] = vol_drv.host_name() - connector['initiator'] = vol_attach.get_iscsi_initiator( - self.adapter) + # Put the values in the connector + connector = {} + wwpn_list = vol_attach.get_wwpns_for_volume_connector( + self.adapter, self.host_uuid, instance) + if wwpn_list is not None: + connector["wwpns"] = wwpn_list + connector['host'] = vol_attach.get_hostname_for_volume(instance) + connector['initiator'] = vol_attach.get_iscsi_initiator(self.adapter) return connector def migrate_disk_and_power_off(self, context, instance, dest, @@ -1720,9 +1702,9 @@ class PowerVMDriver(driver.ComputeDriver): if not conn_info: continue - vol_drv = self._get_inst_vol_adpt(context, instance, - conn_info=conn_info, - stg_ftsk=stg_ftsk) + vol_drv = vol_attach.build_volume_driver( + self.adapter, self.host_uuid, instance, conn_info, + stg_ftsk=stg_ftsk) yield bdm, vol_drv def _build_vol_drivers(self, context, instance, block_device_info=None, @@ -1836,51 +1818,20 @@ class PowerVMDriver(driver.ComputeDriver): # All operations for deploy/destroy require scsi by default. This is # either vopt, local/SSP disks, etc... xags = {pvm_const.XAG.VIO_SMAP} - if not bdms: - LOG.debug('Instance XAGs for VM %(inst)s is %(xags)s.', - {'inst': instance.name, - 'xags': ','.join(xags)}) - return list(xags) + + # BDMs could be none, if there are no cinder volumes. + bdms = bdms if bdms else [] + # If we have any volumes, add the volumes required mapping XAGs. - adp_type = VOLUME_DRIVER_MAPPINGS[CONF.powervm.volume_adapter] - vol_cls = importutils.import_class(adp_type) - xags.update(set(vol_cls.min_xags())) + for bdm in bdms: + driver_type = bdm.get('connection_info').get('driver_volume_type') + vol_cls = vol_attach.get_volume_class(driver_type) + xags.update(set(vol_cls.min_xags())) + LOG.debug('Instance XAGs for VM %(inst)s is %(xags)s.', - {'inst': instance.name, - 'xags': ','.join(xags)}) + {'inst': instance.name, 'xags': ','.join(xags)}) return list(xags) - def _get_inst_vol_adpt(self, context, instance, conn_info=None, - stg_ftsk=None): - """Returns the appropriate volume driver based on connection type. - - Checks the connection info for connection-type and return the - connector, if no connection info is provided returns the default - connector. - :param context: security context - :param instance: Nova instance for which the volume adapter is needed. - :param conn_info: BDM connection information of the instance to - get the volume adapter type (vSCSI/NPIV) requested. - :param stg_ftsk: (Optional) The FeedTask that can be used to defer the - mapping actions against the Virtual I/O Server for. If - not provided, then the connect/disconnect actions will - be immediate. - :return: Returns the volume adapter, if conn_info is not passed then - returns the volume adapter based on the CONF - fc_attach_strategy property (npiv/vscsi). Otherwise returns - the adapter based on the connection-type of - connection_info. - """ - adp_type = VOLUME_DRIVER_MAPPINGS[CONF.powervm.volume_adapter] - vol_cls = importutils.import_class(adp_type) - if conn_info: - LOG.debug('Volume Adapter returned for connection_info=%s', - conn_info) - LOG.debug('Volume Adapter class %(cls)s for instance %(inst)s', - {'cls': vol_cls.__name__, 'inst': instance.name}) - return vol_cls(self.adapter, self.host_uuid, - instance, conn_info, stg_ftsk=stg_ftsk) - def _get_boot_connectivity_type(self, context, bdms, block_device_info): """Get connectivity information for the instance. diff --git a/nova_powervm/virt/powervm/volume/__init__.py b/nova_powervm/virt/powervm/volume/__init__.py index d64eec3a..d459c846 100644 --- a/nova_powervm/virt/powervm/volume/__init__.py +++ b/nova_powervm/virt/powervm/volume/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2015, 2016 IBM Corp. +# Copyright 2015, 2017 IBM Corp. # # All Rights Reserved. # @@ -20,7 +20,11 @@ from pypowervm.tasks import partition from pypowervm.wrappers import virtual_io_server as pvm_vios # Defines the various volume connectors that can be used. +from nova import exception +from oslo_utils import importutils + from nova_powervm import conf as cfg +from nova_powervm.virt.powervm.i18n import _ CONF = cfg.CONF @@ -28,10 +32,57 @@ FC_STRATEGY_MAPPING = { 'npiv': CONF.powervm.fc_npiv_adapter_api, 'vscsi': CONF.powervm.fc_vscsi_adapter_api } -NETWORK_STRATEGY_MAPPING = { - 'iscsi': 'nova_powervm.virt.powervm.volume.iscsi.IscsiVolumeAdapter' + +_STATIC_VOLUME_MAPPINGS = { + 'iscsi': 'nova_powervm.virt.powervm.volume.iscsi.' + 'IscsiVolumeAdapter', + 'local': 'nova_powervm.virt.powervm.volume.local.' + 'LocalVolumeAdapter', + 'nfs': 'nova_powervm.virt.powervm.volume.nfs.NFSVolumeAdapter', + 'gpfs': 'nova_powervm.virt.powervm.volume.gpfs.GPFSVolumeAdapter', } + +def build_volume_driver(adapter, host_uuid, instance, conn_info, + stg_ftsk=None): + vol_cls = get_volume_class(conn_info.get('driver_volume_type')) + + return vol_cls(adapter, host_uuid, instance, conn_info, + stg_ftsk=stg_ftsk) + + +def get_volume_class(drv_type): + if drv_type in _STATIC_VOLUME_MAPPINGS: + class_type = _STATIC_VOLUME_MAPPINGS[drv_type] + elif drv_type == 'fibre_channel': + class_type = (FC_STRATEGY_MAPPING[ + CONF.powervm.fc_attach_strategy.lower()]) + else: + failure_reason = _("Invalid connection type of %s") % drv_type + raise exception.InvalidVolume(reason=failure_reason) + + return importutils.import_class(class_type) + + +def get_hostname_for_volume(instance): + if CONF.powervm.fc_attach_strategy.lower() == 'npiv': + # Tie the host name to the instance, as it will be represented in + # the backend as a full server. + host = CONF.host if len(CONF.host) < 20 else CONF.host[:20] + return host + '_' + instance.name + else: + return CONF.host + + +def get_wwpns_for_volume_connector(adapter, host_uuid, instance): + # WWPNs are derived from the FC connector. Pass in a fake connection info + # to trick it into thinking it FC + fake_fc_conn_info = {'driver_volume_type': 'fibre_channel'} + fc_vol_drv = build_volume_driver(adapter, host_uuid, instance, + fake_fc_conn_info) + return fc_vol_drv.wwpns() + + _ISCSI_INITIATOR = None _ISCSI_LOOKUP_COMPLETE = False diff --git a/nova_powervm/virt/powervm/volume/driver.py b/nova_powervm/virt/powervm/volume/driver.py index bf79a4e0..d1c4e69a 100644 --- a/nova_powervm/virt/powervm/volume/driver.py +++ b/nova_powervm/virt/powervm/volume/driver.py @@ -265,10 +265,3 @@ class FibreChannelVolumeAdapter(PowerVMVolumeAdapter): :return: The list of WWPNs that need to be included in the zone set. """ raise NotImplementedError() - - def host_name(self): - """Derives the host name that should be used for the storage device. - - :return: The host name. - """ - raise NotImplementedError() diff --git a/nova_powervm/virt/powervm/volume/fileio.py b/nova_powervm/virt/powervm/volume/fileio.py new file mode 100644 index 00000000..eb219b50 --- /dev/null +++ b/nova_powervm/virt/powervm/volume/fileio.py @@ -0,0 +1,97 @@ +# Copyright 2017 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 + +from nova_powervm import conf as cfg +from nova_powervm.virt.powervm.i18n import _LI +from nova_powervm.virt.powervm.volume import driver as v_driver +from oslo_log import log as logging +from pypowervm import const as pvm_const +from pypowervm.tasks import partition +from pypowervm.tasks import scsi_mapper as tsk_map +from pypowervm.wrappers import storage as pvm_stg + +CONF = cfg.CONF +LOG = logging.getLogger(__name__) + + +@six.add_metaclass(abc.ABCMeta) +class FileIOVolumeAdapter(v_driver.PowerVMVolumeAdapter): + """Base class for connecting file based Cinder Volumes to PowerVM VMs.""" + + @classmethod + def min_xags(cls): + return [pvm_const.XAG.VIO_SMAP] + + @abc.abstractmethod + def _get_path(self): + """Return the path to the file to connect.""" + pass + + def _connect_volume(self, slot_mgr): + # Get the hosting UUID + nl_vios_wrap = partition.get_mgmt_partition(self.adapter) + vios_uuid = nl_vios_wrap.uuid + + # Get the File Path + fio = pvm_stg.FileIO.bld(self.adapter, self._get_path()) + + def add_func(vios_w): + # If the vios doesn't match, just return + if vios_w.uuid != vios_uuid: + return None + + LOG.info(_LI("Adding logical volume disk connection between VM " + "%(vm)s and VIOS %(vios)s."), + {'vm': self.instance.name, 'vios': vios_w.name}, + instance=self.instance) + mapping = tsk_map.build_vscsi_mapping( + self.host_uuid, vios_w, self.vm_uuid, fio) + return tsk_map.add_map(vios_w, mapping) + + self.stg_ftsk.add_functor_subtask(add_func) + + def _disconnect_volume(self, slot_mgr): + # Get the hosting UUID + nl_vios_wrap = partition.get_mgmt_partition(self.adapter) + vios_uuid = nl_vios_wrap.uuid + + # Build the match function + match_func = tsk_map.gen_match_func(pvm_stg.FileIO, + names=[self._get_path()]) + + # Make sure the remove function will run within the transaction manager + def rm_func(vios_w): + # If the vios doesn't match, just return + if vios_w.uuid != vios_uuid: + return None + + LOG.info(_LI("Disconnecting instance %(inst)s from storage " + "disks."), {'inst': self.instance.name}, + instance=self.instance) + return tsk_map.remove_maps(vios_w, self.vm_uuid, + match_func=match_func) + + self.stg_ftsk.add_functor_subtask(rm_func) + + # Find the disk directly. + mappings = tsk_map.find_maps(nl_vios_wrap.scsi_mappings, + client_lpar_id=self.vm_uuid, + match_func=match_func) + + return [x.backing_storage for x in mappings] diff --git a/nova_powervm/virt/powervm/volume/gpfs.py b/nova_powervm/virt/powervm/volume/gpfs.py new file mode 100644 index 00000000..79067a6c --- /dev/null +++ b/nova_powervm/virt/powervm/volume/gpfs.py @@ -0,0 +1,24 @@ +# Copyright 2017 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_powervm.virt.powervm.volume import fileio + + +class GPFSVolumeAdapter(fileio.FileIOVolumeAdapter): + """Connects GPFS Cinder Volumes to PowerVM VMs.""" + + def _get_path(self): + return self.connection_info.get("data")['device_path'] diff --git a/nova_powervm/virt/powervm/volume/iscsi.py b/nova_powervm/virt/powervm/volume/iscsi.py index 5ca62df3..fc36ba94 100644 --- a/nova_powervm/virt/powervm/volume/iscsi.py +++ b/nova_powervm/virt/powervm/volume/iscsi.py @@ -56,9 +56,6 @@ class IscsiVolumeAdapter(volume.VscsiVolumeAdapter, """The type of volume supported by this type.""" return 'iscsi' - def host_name(self): - return CONF.host - @classmethod def min_xags(cls): """List of pypowervm XAGs needed to support this adapter.""" diff --git a/nova_powervm/virt/powervm/volume/local.py b/nova_powervm/virt/powervm/volume/local.py new file mode 100644 index 00000000..12f0fad6 --- /dev/null +++ b/nova_powervm/virt/powervm/volume/local.py @@ -0,0 +1,24 @@ +# Copyright 2017 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_powervm.virt.powervm.volume import fileio + + +class LocalVolumeAdapter(fileio.FileIOVolumeAdapter): + """Connects Local Cinder Volumes to PowerVM VMs.""" + + def _get_path(self): + return self.connection_info['data']['device_path'] diff --git a/nova_powervm/virt/powervm/volume/nfs.py b/nova_powervm/virt/powervm/volume/nfs.py new file mode 100644 index 00000000..082e0f23 --- /dev/null +++ b/nova_powervm/virt/powervm/volume/nfs.py @@ -0,0 +1,26 @@ +# Copyright 2017 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_powervm.virt.powervm.volume import fileio +import os + + +class NFSVolumeAdapter(fileio.FileIOVolumeAdapter): + """Connects NFS Cinder Volumes to PowerVM VMs.""" + + def _get_path(self): + return os.path.join(self.connection_info['data']['export'], + self.connection_info['data']['name']) diff --git a/nova_powervm/virt/powervm/volume/npiv.py b/nova_powervm/virt/powervm/volume/npiv.py index f81cf819..cbc17b9b 100644 --- a/nova_powervm/virt/powervm/volume/npiv.py +++ b/nova_powervm/virt/powervm/volume/npiv.py @@ -592,14 +592,6 @@ class NPIVVolumeAdapter(v_driver.FibreChannelVolumeAdapter): "%(fabric)s."), {'fabric': fabric}, instance=self.instance) - def host_name(self): - """Derives the host name that should be used for the storage device. - - :return: The host name. - """ - host = CONF.host if len(CONF.host) < 20 else CONF.host[:20] - return host + '_' + self.instance.name - def _set_fabric_state(self, fabric, state): """Sets the fabric state into the instance's system metadata. diff --git a/nova_powervm/virt/powervm/volume/vscsi.py b/nova_powervm/virt/powervm/volume/vscsi.py index 8df14e28..1036d619 100644 --- a/nova_powervm/virt/powervm/volume/vscsi.py +++ b/nova_powervm/virt/powervm/volume/vscsi.py @@ -355,13 +355,6 @@ class PVVscsiFCVolumeAdapter(volume.VscsiVolumeAdapter, _vscsi_pfc_wwpns = pvm_tpar.get_physical_wwpns(self.adapter) return _vscsi_pfc_wwpns - def host_name(self): - """Derives the host name that should be used for the storage device. - - :return: The host name. - """ - return CONF.host - def _get_hdisk_itls(self, vios_w): """Returns the mapped ITLs for the hdisk for the given VIOS.