Merge "Add support for ceph volumes"
This commit is contained in:
commit
a584585074
|
@ -0,0 +1,191 @@
|
|||
# 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 import exception as p_exc
|
||||
from nova_powervm.virt.powervm.volume import rbd as v_drv
|
||||
from pypowervm import const as pvm_const
|
||||
from pypowervm.tests import test_fixtures as pvm_fx
|
||||
from pypowervm.wrappers import base_partition as pvm_bp
|
||||
from pypowervm.wrappers import storage as pvm_stg
|
||||
from pypowervm.wrappers import virtual_io_server as pvm_vios
|
||||
|
||||
|
||||
class FakeRBDVolAdapter(v_drv.RBDVolumeAdapter):
|
||||
"""Subclass for RBDVolumeAdapter, since it is abstract."""
|
||||
|
||||
def __init__(self, adapter, host_uuid, instance, connection_info,
|
||||
stg_ftsk=None):
|
||||
super(FakeRBDVolAdapter, self).__init__(
|
||||
adapter, host_uuid, instance, connection_info, stg_ftsk=stg_ftsk)
|
||||
|
||||
|
||||
class TestRBDVolumeAdapter(test_vol.TestVolumeAdapter):
|
||||
"""Tests the RBDVolumeAdapter. NovaLink is a I/O host."""
|
||||
|
||||
def setUp(self):
|
||||
super(TestRBDVolumeAdapter, self).setUp()
|
||||
|
||||
# Needed for the volume adapter
|
||||
self.adpt = self.useFixture(pvm_fx.AdapterFx()).adpt
|
||||
mock_inst = mock.MagicMock(uuid='2BC123')
|
||||
|
||||
self.vol_drv = FakeRBDVolAdapter(self.adpt, 'host_uuid', mock_inst,
|
||||
{'data': {'name': 'pool/image'},
|
||||
'serial': 'volid1'})
|
||||
|
||||
self.fake_vios = pvm_vios.VIOS.bld(
|
||||
self.adpt, 'vios1',
|
||||
pvm_bp.PartitionMemoryConfiguration.bld(self.adpt, 1024),
|
||||
pvm_bp.PartitionMemoryConfiguration.bld(self.adpt, 0.1, 1))
|
||||
self.feed = [pvm_vios.VIOS.wrap(self.fake_vios.entry)]
|
||||
ftskfx = pvm_fx.FeedTaskFx(self.feed)
|
||||
self.useFixture(ftskfx)
|
||||
|
||||
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.scsi_mapper.add_map', autospec=True)
|
||||
@mock.patch('pypowervm.tasks.scsi_mapper.build_vscsi_mapping',
|
||||
autospec=True)
|
||||
@mock.patch('pypowervm.entities.Entry.uuid',
|
||||
new_callable=mock.PropertyMock)
|
||||
@mock.patch('pypowervm.tasks.slot_map.SlotMapStore.register_vscsi_mapping',
|
||||
autospec=True)
|
||||
@mock.patch('pypowervm.tasks.client_storage.udid_to_scsi_mapping',
|
||||
autospec=True)
|
||||
@mock.patch('nova_powervm.virt.powervm.vm.get_vm_id')
|
||||
@mock.patch('pypowervm.tasks.partition.get_mgmt_partition', autospec=True)
|
||||
@mock.patch('pypowervm.wrappers.storage.RBD.bld_ref')
|
||||
def test_connect_volume(self, mock_rbd_bld_ref, mock_get_mgmt_partition,
|
||||
mock_get_vm_id, mock_udid_to_map, mock_reg_map,
|
||||
mock_get_vios_uuid, mock_build_map, mock_add_map):
|
||||
# Mockups
|
||||
mock_rbd = mock.Mock()
|
||||
mock_rbd_bld_ref.return_value = mock_rbd
|
||||
mock_slot_mgr = mock.MagicMock()
|
||||
mock_slot_mgr.build_map.get_vscsi_slot.return_value = 4, 'fake_path'
|
||||
|
||||
mock_vios = mock.Mock(uuid='uuid1')
|
||||
mock_get_mgmt_partition.return_value = mock_vios
|
||||
mock_get_vios_uuid.return_value = 'uuid1'
|
||||
mock_get_vm_id.return_value = 'partition_id'
|
||||
|
||||
mock_udid_to_map.return_value = mock.Mock()
|
||||
mock_add_map.return_value = None
|
||||
|
||||
# Invoke
|
||||
self.vol_drv.connect_volume(mock_slot_mgr)
|
||||
|
||||
# Validate
|
||||
mock_rbd_bld_ref.assert_called_once_with(
|
||||
self.adpt, 'pool/image')
|
||||
self.assertEqual(1, mock_build_map.call_count)
|
||||
self.assertEqual(1, mock_udid_to_map.call_count)
|
||||
|
||||
@mock.patch('pypowervm.tasks.scsi_mapper.build_vscsi_mapping',
|
||||
autospec=True)
|
||||
@mock.patch('pypowervm.entities.Entry.uuid',
|
||||
new_callable=mock.PropertyMock)
|
||||
@mock.patch('pypowervm.tasks.slot_map.SlotMapStore.register_vscsi_mapping',
|
||||
autospec=True)
|
||||
@mock.patch('pypowervm.tasks.client_storage.udid_to_scsi_mapping',
|
||||
autospec=True)
|
||||
@mock.patch('nova_powervm.virt.powervm.vm.get_vm_id')
|
||||
@mock.patch('pypowervm.tasks.partition.get_mgmt_partition', autospec=True)
|
||||
@mock.patch('pypowervm.wrappers.storage.RBD.bld_ref')
|
||||
def test_connect_volume_rebuild_no_slot(
|
||||
self, mock_rbd_bld_ref, mock_get_mgmt_partition, mock_get_vm_id,
|
||||
mock_udid_to_map, mock_reg_map, mock_get_vios_uuid, mock_build_map):
|
||||
# Mockups
|
||||
mock_rbd = mock.Mock()
|
||||
mock_rbd_bld_ref.return_value = mock_rbd
|
||||
mock_slot_mgr = mock.MagicMock()
|
||||
mock_slot_mgr.is_rebuild = True
|
||||
mock_slot_mgr.build_map.get_vscsi_slot.return_value = None, None
|
||||
|
||||
mock_vios = mock.Mock(uuid='uuid1')
|
||||
mock_get_mgmt_partition.return_value = mock_vios
|
||||
mock_get_vios_uuid.return_value = 'uuid1'
|
||||
# Invoke
|
||||
self.vol_drv.connect_volume(mock_slot_mgr)
|
||||
|
||||
# Validate
|
||||
mock_rbd_bld_ref.assert_called_once_with(
|
||||
self.adpt, 'pool/image')
|
||||
self.assertEqual(0, mock_build_map.call_count)
|
||||
|
||||
@mock.patch('pypowervm.entities.Entry.uuid',
|
||||
new_callable=mock.PropertyMock)
|
||||
@mock.patch('pypowervm.tasks.partition.get_mgmt_partition', autospec=True)
|
||||
@mock.patch('pypowervm.tasks.scsi_mapper.gen_match_func', autospec=True)
|
||||
@mock.patch('pypowervm.tasks.scsi_mapper.find_maps', autospec=True)
|
||||
def test_disconnect_volume(self, mock_find_maps, mock_gen_match_func,
|
||||
mock_get_mgmt_partition, mock_entry_uuid):
|
||||
# Mockups
|
||||
mock_slot_mgr = mock.MagicMock()
|
||||
|
||||
mock_vios = mock.Mock(uuid='uuid1')
|
||||
mock_get_mgmt_partition.return_value = mock_vios
|
||||
|
||||
mock_match_func = mock.Mock()
|
||||
mock_gen_match_func.return_value = mock_match_func
|
||||
mock_entry_uuid.return_value = 'uuid1'
|
||||
# Invoke
|
||||
self.vol_drv._disconnect_volume(mock_slot_mgr)
|
||||
|
||||
# Validate
|
||||
mock_gen_match_func.assert_called_once_with(
|
||||
pvm_stg.VDisk, names=['pool/image'])
|
||||
mock_find_maps.assert_called_once_with(
|
||||
mock.ANY,
|
||||
client_lpar_id='2BC123', match_func=mock_match_func)
|
||||
|
||||
@mock.patch('nova_powervm.virt.powervm.volume.rbd.RBDVolumeAdapter.'
|
||||
'vios_uuids', new_callable=mock.PropertyMock)
|
||||
@mock.patch('nova_powervm.virt.powervm.volume.rbd.RBDVolumeAdapter.'
|
||||
'is_volume_on_vios')
|
||||
def test_pre_live_migration_on_destination(self, mock_on_vios, mock_uuids):
|
||||
mock_uuids.return_value = ['uuid1']
|
||||
mock_on_vios.return_value = (True, 'pool/image')
|
||||
self.vol_drv.pre_live_migration_on_destination(mock.ANY)
|
||||
mock_on_vios.return_value = (False, None)
|
||||
self.assertRaises(
|
||||
p_exc.VolumePreMigrationFailed,
|
||||
self.vol_drv.pre_live_migration_on_destination, mock.ANY)
|
||||
|
||||
@mock.patch('nova_powervm.virt.powervm.volume.rbd.RBDVolumeAdapter.'
|
||||
'vios_uuids', new_callable=mock.PropertyMock)
|
||||
@mock.patch('pypowervm.tasks.hdisk.rbd_exists', autospec=True)
|
||||
def test_is_volume_on_vios(self, mock_exists, mock_vios_uuids):
|
||||
mock_exists.return_value = True
|
||||
mock_vios_uuids.return_value = ['uuid1']
|
||||
vol_found, vol_name = self.vol_drv.is_volume_on_vios(
|
||||
mock.Mock(uuid='uuid2'))
|
||||
self.assertFalse(vol_found)
|
||||
self.assertIsNone(vol_name)
|
||||
vol_found, vol_name = self.vol_drv.is_volume_on_vios(
|
||||
mock.Mock(uuid='uuid1'))
|
||||
self.assertTrue(vol_found)
|
||||
self.assertEqual('pool/image', vol_name)
|
||||
mock_exists.return_value = False
|
||||
vol_found, vol_name = self.vol_drv.is_volume_on_vios(
|
||||
mock.Mock(uuid='uuid1'))
|
||||
self.assertFalse(vol_found)
|
||||
self.assertIsNone(vol_name)
|
|
@ -124,7 +124,7 @@ class NovaSlotManager(slot_map.SlotMapStore):
|
|||
pv_vscsi_vol_to_vio = {}
|
||||
fabric_names = []
|
||||
for bdm, vol_drv in vol_drv_iter:
|
||||
if vol_drv.vol_type() in ['vscsi', 'fileio']:
|
||||
if vol_drv.vol_type() in ['vscsi', 'fileio', 'rbd']:
|
||||
self._pv_vscsi_vol_to_vio(vol_drv, pv_vscsi_vol_to_vio)
|
||||
elif len(fabric_names) == 0 and vol_drv.vol_type() == 'npiv':
|
||||
fabric_names = vol_drv._fabric_names()
|
||||
|
|
|
@ -41,6 +41,7 @@ _STATIC_VOLUME_MAPPINGS = {
|
|||
'LocalVolumeAdapter',
|
||||
'nfs': 'nova_powervm.virt.powervm.volume.nfs.NFSVolumeAdapter',
|
||||
'gpfs': 'nova_powervm.virt.powervm.volume.gpfs.GPFSVolumeAdapter',
|
||||
'rbd': 'nova_powervm.virt.powervm.volume.rbd.RBDVolumeAdapter',
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,180 @@
|
|||
# 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 taskflow import task
|
||||
|
||||
from nova_powervm import conf as cfg
|
||||
from nova_powervm.virt.powervm import exception as p_exc
|
||||
from nova_powervm.virt.powervm import vm
|
||||
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 client_storage as pvm_c_stor
|
||||
from pypowervm.tasks import hdisk
|
||||
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 RBDVolumeAdapter(v_driver.PowerVMVolumeAdapter):
|
||||
"""Base class for connecting ceph based Cinder Volumes to PowerVM VMs."""
|
||||
|
||||
def __init__(self, adapter, host_uuid, instance, connection_info,
|
||||
stg_ftsk=None):
|
||||
super(RBDVolumeAdapter, self).__init__(
|
||||
adapter, host_uuid, instance, connection_info, stg_ftsk=stg_ftsk)
|
||||
self._nl_vios_ids = None
|
||||
|
||||
@classmethod
|
||||
def min_xags(cls):
|
||||
return [pvm_const.XAG.VIO_SMAP]
|
||||
|
||||
@classmethod
|
||||
def vol_type(cls):
|
||||
"""The type of volume supported by this type."""
|
||||
return 'rbd'
|
||||
|
||||
@property
|
||||
def vios_uuids(self):
|
||||
"""List the UUIDs of the Virtual I/O Servers hosting the storage."""
|
||||
# Get the hosting UUID
|
||||
if self._nl_vios_ids is None:
|
||||
nl_vios_wrap = partition.get_mgmt_partition(self.adapter)
|
||||
self._nl_vios_ids = [nl_vios_wrap.uuid]
|
||||
return self._nl_vios_ids
|
||||
|
||||
def pre_live_migration_on_destination(self, mig_data):
|
||||
"""Perform pre live migration steps for the volume on the target host.
|
||||
|
||||
This method performs any pre live migration that is needed.
|
||||
|
||||
This method will be called after the pre_live_migration_on_source
|
||||
method. The data from the pre_live call will be passed in via the
|
||||
mig_data. This method should put its output into the dest_mig_data.
|
||||
|
||||
:param mig_data: Dict of migration data for the destination server.
|
||||
If the volume connector needs to provide
|
||||
information to the live_migration command, it
|
||||
should be added to this dictionary.
|
||||
"""
|
||||
for vios_uuid in self.vios_uuids:
|
||||
exists, name = self.is_volume_on_vios(vios_uuid)
|
||||
if exists and name is not None:
|
||||
return
|
||||
name = self.connection_info["data"]["name"]
|
||||
LOG.warning("RBD %s not found", name, instance=self.instance)
|
||||
raise p_exc.VolumePreMigrationFailed(
|
||||
volume_id=self.volume_id, instance_name=self.instance.name)
|
||||
|
||||
def _connect_volume(self, slot_mgr):
|
||||
name = self.connection_info["data"]["name"]
|
||||
# Get the File Path
|
||||
rbd = pvm_stg.RBD.bld_ref(self.adapter, name)
|
||||
|
||||
def add_func(vios_w):
|
||||
# If the vios doesn't match, just return
|
||||
if vios_w.uuid not in self.vios_uuids:
|
||||
return None
|
||||
|
||||
LOG.info("Adding rbd disk connection to VIOS %(vios)s.",
|
||||
{'vios': vios_w.name}, instance=self.instance)
|
||||
slot, lua = slot_mgr.build_map.get_vscsi_slot(vios_w, name)
|
||||
if slot_mgr.is_rebuild and not slot:
|
||||
LOG.debug('Detected a device with path %(path)s on VIOS '
|
||||
'%(vios)s on the rebuild that did not exist on the '
|
||||
'source. Ignoring.',
|
||||
{'path': name, 'vios': vios_w.uuid},
|
||||
instance=self.instance)
|
||||
return None
|
||||
|
||||
mapping = tsk_map.build_vscsi_mapping(
|
||||
self.host_uuid, vios_w, self.vm_uuid, rbd, lpar_slot_num=slot,
|
||||
lua=lua)
|
||||
return tsk_map.add_map(vios_w, mapping)
|
||||
|
||||
self.stg_ftsk.add_functor_subtask(add_func)
|
||||
|
||||
# Run after all the deferred tasks the query to save the slots in the
|
||||
# slot map.
|
||||
def set_slot_info():
|
||||
vios_wraps = self.stg_ftsk.feed
|
||||
partition_id = vm.get_vm_id(self.adapter, self.vm_uuid)
|
||||
for vios_w in vios_wraps:
|
||||
scsi_map = pvm_c_stor.udid_to_scsi_mapping(
|
||||
vios_w, name, partition_id)
|
||||
if not scsi_map:
|
||||
continue
|
||||
slot_mgr.register_vscsi_mapping(scsi_map)
|
||||
|
||||
self.stg_ftsk.add_post_execute(task.FunctorTask(
|
||||
set_slot_info, name='rbd_slot_%s' % name))
|
||||
|
||||
def _disconnect_volume(self, slot_mgr):
|
||||
# Build the match function
|
||||
name = self.connection_info["data"]["name"]
|
||||
match_func = tsk_map.gen_match_func(pvm_stg.VDisk,
|
||||
names=[name])
|
||||
|
||||
# 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 not in self.vios_uuids:
|
||||
return None
|
||||
|
||||
LOG.info("Disconnecting instance %(inst)s from storage "
|
||||
"disks.", {'inst': self.instance.name},
|
||||
instance=self.instance)
|
||||
removed_maps = tsk_map.remove_maps(vios_w, self.vm_uuid,
|
||||
match_func=match_func)
|
||||
for rm_map in removed_maps:
|
||||
slot_mgr.drop_vscsi_mapping(rm_map)
|
||||
return removed_maps
|
||||
|
||||
self.stg_ftsk.add_functor_subtask(rm_func)
|
||||
# Find the disk directly.
|
||||
vios_w = self.stg_ftsk.wrapper_tasks[self.vios_uuids[0]].wrapper
|
||||
mappings = tsk_map.find_maps(vios_w.scsi_mappings,
|
||||
client_lpar_id=self.vm_uuid,
|
||||
match_func=match_func)
|
||||
|
||||
return [x.backing_storage for x in mappings]
|
||||
|
||||
def is_volume_on_vios(self, vio):
|
||||
"""Returns whether or not the volume file is on a VIOS.
|
||||
|
||||
This method is used during live-migration and rebuild to
|
||||
check if the volume is available on the target host.
|
||||
|
||||
:param vio: The Virtual I/O Server wrapper or UDID string of the VIOS.
|
||||
:return: True if the file is on the VIOS. False
|
||||
otherwise.
|
||||
:return: The file path.
|
||||
"""
|
||||
try:
|
||||
vio_uuid = vio.uuid
|
||||
except AttributeError:
|
||||
vio_uuid = vio
|
||||
if vio_uuid not in self.vios_uuids:
|
||||
return False, None
|
||||
name = self.connection_info["data"]["name"]
|
||||
exists = hdisk.rbd_exists(self.adapter, vio_uuid, name)
|
||||
return exists, name if exists else None
|
Loading…
Reference in New Issue