Merge "PowerVM Driver: DiskAdapter parent class"

This commit is contained in:
Zuul 2018-05-17 16:24:09 +00:00 committed by Gerrit Code Review
commit 050e9202f6
5 changed files with 402 additions and 153 deletions

View File

@ -0,0 +1,51 @@
# Copyright 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.
from nova.virt.powervm.disk import driver as disk_dvr
class FakeDiskAdapter(disk_dvr.DiskAdapter):
"""A fake subclass of DiskAdapter.
This is done so that the abstract methods/properties can be stubbed and the
class can be instantiated for testing.
"""
def _vios_uuids(self):
pass
def _disk_match_func(self, disk_type, instance):
pass
def disconnect_disk_from_mgmt(self, vios_uuid, disk_name):
pass
def capacity(self):
pass
def capacity_used(self):
pass
def detach_disk(self, instance):
pass
def delete_disks(self, storage_elems):
pass
def create_disk_from_image(self, context, instance, image_meta):
pass
def attach_disk(self, instance, disk_info, stg_ftsk):
pass

View File

@ -0,0 +1,59 @@
# 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 fixtures
import mock
from pypowervm import const as pvm_const
from nova import test
from nova.tests.unit.virt.powervm.disk import fake_adapter
class TestDiskAdapter(test.NoDBTestCase):
"""Unit Tests for the generic storage driver."""
def setUp(self):
super(TestDiskAdapter, self).setUp()
# Return the mgmt uuid
self.mgmt_uuid = self.useFixture(fixtures.MockPatch(
'nova.virt.powervm.mgmt.mgmt_uuid')).mock
self.mgmt_uuid.return_value = 'mp_uuid'
# The values (adapter and host uuid) are not used in the base.
# Default them to None. We use the fake adapter here because we can't
# instantiate DiskAdapter which is an abstract base class.
self.st_adpt = fake_adapter.FakeDiskAdapter(None, None)
@mock.patch("pypowervm.util.sanitize_file_name_for_api")
def test_get_disk_name(self, mock_san):
inst = mock.Mock()
inst.configure_mock(name='a_name_that_is_longer_than_eight',
uuid='01234567-abcd-abcd-abcd-123412341234')
# Long
self.assertEqual(mock_san.return_value,
self.st_adpt._get_disk_name('type', inst))
mock_san.assert_called_with(inst.name, prefix='type_',
max_len=pvm_const.MaxLen.FILENAME_DEFAULT)
mock_san.reset_mock()
# Short
self.assertEqual(mock_san.return_value,
self.st_adpt._get_disk_name('type', inst, short=True))
mock_san.assert_called_with('a_name_t_0123', prefix='t_',
max_len=pvm_const.MaxLen.VDISK_NAME)

View File

@ -122,6 +122,7 @@ class TestSSPDiskAdapter(test.NoDBTestCase):
def test_capabilities(self):
self.assertTrue(self.ssp_drv.capabilities.get('shared_storage'))
self.assertFalse(self.ssp_drv.capabilities.get('has_imagecache'))
self.assertTrue(self.ssp_drv.capabilities.get('snapshot'))
@mock.patch('pypowervm.util.get_req_path_uuid', autospec=True)
@ -164,7 +165,7 @@ class TestSSPDiskAdapter(test.NoDBTestCase):
@mock.patch('pypowervm.util.sanitize_file_name_for_api', autospec=True)
@mock.patch('pypowervm.tasks.storage.crt_lu', autospec=True)
@mock.patch('nova.image.api.API.download')
@mock.patch('nova.virt.powervm.disk.ssp.IterableToFileAdapter',
@mock.patch('nova.virt.powervm.disk.driver.IterableToFileAdapter',
autospec=True)
def test_create_disk_from_image(self, mock_it2f, mock_dl, mock_crt_lu,
mock_san, mock_vuuid, mock_goru):
@ -342,26 +343,6 @@ class TestSSPDiskAdapter(test.NoDBTestCase):
mock_disk_name.assert_called_once_with('disk_type', 'instance')
mock_gen_match.assert_called_with(pvm_stg.LU, names=['disk_name'])
@mock.patch("pypowervm.util.sanitize_file_name_for_api", autospec=True)
def test_get_disk_name(self, mock_san):
inst = mock.Mock()
inst.configure_mock(name='a_name_that_is_longer_than_eight',
uuid='01234567-abcd-abcd-abcd-123412341234')
# Long
self.assertEqual(mock_san.return_value,
self.ssp_drv._get_disk_name('type', inst))
mock_san.assert_called_with(inst.name, prefix='type_',
max_len=pvm_const.MaxLen.FILENAME_DEFAULT)
mock_san.reset_mock()
# Short
self.assertEqual(mock_san.return_value,
self.ssp_drv._get_disk_name('type', inst, short=True))
mock_san.assert_called_with('a_name_t_0123', prefix='t_',
max_len=pvm_const.MaxLen.VDISK_NAME)
@mock.patch('nova.virt.powervm.disk.ssp.SSPDiskAdapter.'
'_vios_uuids', new_callable=mock.PropertyMock)
@mock.patch('nova.virt.powervm.vm.get_instance_wrapper', autospec=True)

View File

@ -0,0 +1,276 @@
# 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 abc
import oslo_log.log as logging
import pypowervm.const as pvm_const
import pypowervm.tasks.scsi_mapper as tsk_map
import pypowervm.util as pvm_u
import pypowervm.wrappers.virtual_io_server as pvm_vios
import six
from nova import exception
from nova.virt.powervm import mgmt
from nova.virt.powervm import vm
LOG = logging.getLogger(__name__)
class DiskType(object):
BOOT = 'boot'
IMAGE = 'image'
class IterableToFileAdapter(object):
"""A degenerate file-like so that an iterable can be read like a file.
The 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 = next(self.iterator)
except StopIteration:
return ''
return_value = chunk[0:size]
self.remaining_data = chunk[size:]
return return_value
@six.add_metaclass(abc.ABCMeta)
class DiskAdapter(object):
capabilities = {
'shared_storage': False,
'has_imagecache': False,
'snapshot': False,
}
def __init__(self, adapter, host_uuid):
"""Initialize the DiskAdapter.
:param adapter: The pypowervm adapter.
:param host_uuid: The UUID of the PowerVM host.
"""
self._adapter = adapter
self._host_uuid = host_uuid
self.mp_uuid = mgmt.mgmt_uuid(self._adapter)
@abc.abstractproperty
def _vios_uuids(self):
"""List the UUIDs of the Virtual I/O Servers hosting the storage."""
raise NotImplementedError()
@abc.abstractmethod
def _disk_match_func(self, 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, with the
following specification:
def match_func(storage_elem)
param storage_elem: A backing storage element wrapper (VOpt,
VDisk, PV, or LU) to be analyzed.
return: True if the storage_elem's mapping should be included;
False otherwise.
"""
raise NotImplementedError()
def get_bootdisk_path(self, instance, vios_uuid):
"""Find scsi mappings on given VIOS for the instance.
This method finds all scsi mappings on a given vios that are associated
with the instance and disk_type.
:param instance: nova.objects.instance.Instance object owning the
requested disk.
:param vios_uuid: PowerVM UUID of the VIOS to search for mappings.
:return: Iterator of scsi mappings that are associated with the
instance and disk_type or None.
"""
vm_uuid = vm.get_pvm_uuid(instance)
match_func = self._disk_match_func(DiskType.BOOT, instance)
vios_wrap = pvm_vios.VIOS.get(self._adapter, uuid=vios_uuid,
xag=[pvm_const.XAG.VIO_SMAP])
maps = tsk_map.find_maps(vios_wrap.scsi_mappings,
client_lpar_id=vm_uuid, match_func=match_func)
if maps:
return maps[0].server_adapter.backing_dev_name
return None
def _get_bootdisk_iter(self, instance):
"""Return an iterator of (storage_elem, VIOS) tuples for the instance.
This method returns an iterator of (storage_elem, VIOS) tuples, where
storage_element is a pypowervm storage element wrapper associated with
the instance boot disk and VIOS is the wrapper of the Virtual I/O
server owning that storage element.
:param instance: nova.objects.instance.Instance object owning the
requested disk.
:return: Iterator of tuples of (storage_elem, VIOS).
"""
lpar_wrap = vm.get_instance_wrapper(self._adapter, instance)
match_func = self._disk_match_func(DiskType.BOOT, instance)
for vios_uuid in self._vios_uuids:
vios_wrap = pvm_vios.VIOS.get(
self._adapter, uuid=vios_uuid, xag=[pvm_const.XAG.VIO_SMAP])
for scsi_map in tsk_map.find_maps(
vios_wrap.scsi_mappings, client_lpar_id=lpar_wrap.id,
match_func=match_func):
yield scsi_map.backing_storage, vios_wrap
def connect_instance_disk_to_mgmt(self, instance):
"""Connect an instance's boot disk to the management partition.
:param instance: The instance whose boot disk is to be mapped.
:return stg_elem: The storage element (LU, VDisk, etc.) that was mapped
:return vios: The EntryWrapper of the VIOS from which the mapping was
made.
:raise InstanceDiskMappingFailed: If the mapping could not be done.
"""
for stg_elem, vios in self._get_bootdisk_iter(instance):
msg_args = {'disk_name': stg_elem.name, 'vios_name': vios.name}
# Create a new mapping. NOTE: If there's an existing mapping on
# the other VIOS but not this one, we'll create a second mapping
# here. It would take an extreme sequence of events to get to that
# point, and the second mapping would be harmless anyway. The
# alternative would be always checking all VIOSes for existing
# mappings, which increases the response time of the common case by
# an entire GET of VIOS+VIO_SMAP.
LOG.debug("Mapping boot disk %(disk_name)s to the management "
"partition from Virtual I/O Server %(vios_name)s.",
msg_args, instance=instance)
try:
tsk_map.add_vscsi_mapping(self._host_uuid, vios, self.mp_uuid,
stg_elem)
# If that worked, we're done. add_vscsi_mapping logged.
return stg_elem, vios
except Exception:
LOG.exception("Failed to map boot disk %(disk_name)s to the "
"management partition from Virtual I/O Server "
"%(vios_name)s.", msg_args, instance=instance)
# Try the next hit, if available.
# We either didn't find the boot dev, or failed all attempts to map it.
raise exception.InstanceDiskMappingFailed(instance_name=instance.name)
@abc.abstractmethod
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.
"""
raise NotImplementedError()
@abc.abstractproperty
def capacity(self):
"""Capacity of the storage in gigabytes.
Default is to make the capacity arbitrarily large.
"""
raise NotImplementedError()
@abc.abstractproperty
def capacity_used(self):
"""Capacity of the storage in gigabytes that is used.
Default is to say none of it is used.
"""
raise NotImplementedError()
@staticmethod
def _get_disk_name(disk_type, instance, short=False):
"""Generate a name for a virtual disk associated with an instance.
:param disk_type: One of the DiskType enum values.
:param instance: The instance for which the disk is to be created.
:param short: If True, the generated name will be limited to 15
characters (the limit for virtual disk). If False, it
will be limited by the API (79 characters currently).
:return: The sanitized file name for the disk.
"""
prefix = '%s_' % (disk_type[0] if short else disk_type)
base = ('%s_%s' % (instance.name[:8], instance.uuid[:4]) if short
else instance.name)
return pvm_u.sanitize_file_name_for_api(
base, prefix=prefix, max_len=pvm_const.MaxLen.VDISK_NAME if short
else pvm_const.MaxLen.FILENAME_DEFAULT)
@abc.abstractmethod
def detach_disk(self, instance):
"""Detaches the storage adapters from the image disk.
:param instance: instance to detach the image for.
:return: A list of all the backing storage elements that were
detached from the I/O Server and VM.
"""
raise NotImplementedError()
@abc.abstractmethod
def delete_disks(self, storage_elems):
"""Removes the disks specified by the mappings.
:param storage_elems: A list of the storage elements that are to be
deleted. Derived from the return value from
detach_disk.
"""
raise NotImplementedError()
@abc.abstractmethod
def create_disk_from_image(self, context, instance, image_meta):
"""Creates a disk and copies the specified image to it.
Cleans up 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: nova.objects.ImageMeta object with the metadata of
the image of the instance.
:return: The backing pypowervm storage object that was created.
"""
raise NotImplementedError()
@abc.abstractmethod
def attach_disk(self, instance, disk_info, stg_ftsk):
"""Attaches the disk image to the Virtual Machine.
:param instance: nova instance to attach 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: (Optional) The pypowervm transaction FeedTask for the
I/O Operations. If provided, the Virtual I/O Server
mapping updates will be added to the FeedTask. This
defers the updates to some later point in time. If
the FeedTask is not provided, the updates will be run
immediately when this method is executed.
"""
raise NotImplementedError()

View File

@ -24,50 +24,18 @@ from pypowervm.tasks import storage as tsk_stg
import pypowervm.util as pvm_u
import pypowervm.wrappers.cluster as pvm_clust
import pypowervm.wrappers.storage as pvm_stg
import pypowervm.wrappers.virtual_io_server as pvm_vios
from nova import exception
from nova import image
from nova.virt.powervm import mgmt
from nova.virt.powervm.disk import driver as disk_drv
from nova.virt.powervm import vm
LOG = logging.getLogger(__name__)
IMAGE_API = image.API()
class DiskType(object):
BOOT = 'boot'
IMAGE = 'image'
class IterableToFileAdapter(object):
"""A degenerate file-like so that an iterable can be read like a file.
The 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 = next(self.iterator)
except StopIteration:
return ''
return_value = chunk[0:size]
self.remaining_data = chunk[size:]
return return_value
class SSPDiskAdapter(object):
class SSPDiskAdapter(disk_drv.DiskAdapter):
"""Provides a disk adapter for Shared Storage Pools.
Shared Storage Pools are a clustered file system technology that can link
@ -79,6 +47,13 @@ class SSPDiskAdapter(object):
capabilities = {
'shared_storage': True,
# NOTE(efried): Whereas the SSP disk driver definitely does image
# caching, it's not through the nova.virt.imagecache.ImageCacheManager
# API. Setting `has_imagecache` to True here would have the side
# effect of having a periodic task try to call this class's
# manage_image_cache method (not implemented here; and a no-op in the
# superclass) which would be harmless, but unnecessary.
'has_imagecache': False,
'snapshot': True,
}
@ -88,9 +63,8 @@ class SSPDiskAdapter(object):
:param adapter: pypowervm.adapter.Adapter for the PowerVM REST API.
:param host_uuid: PowerVM UUID of the managed system.
"""
self._adapter = adapter
self._host_uuid = host_uuid
self.mp_uuid = mgmt.mgmt_uuid(self._adapter)
super(SSPDiskAdapter, self).__init__(adapter, host_uuid)
try:
self._clust = pvm_clust.Cluster.get(self._adapter)[0]
self._ssp = pvm_stg.SSP.get_by_href(
@ -192,14 +166,14 @@ class SSPDiskAdapter(object):
image_lu = tsk_cs.get_or_upload_image_lu(
self._tier, pvm_u.sanitize_file_name_for_api(
image_meta.name, prefix=DiskType.IMAGE + '_',
image_meta.name, prefix=disk_drv.DiskType.IMAGE + '_',
suffix='_' + image_meta.checksum),
random.choice(self._vios_uuids), IterableToFileAdapter(
random.choice(self._vios_uuids), disk_drv.IterableToFileAdapter(
IMAGE_API.download(context, image_meta.id)), image_meta.size,
upload_type=tsk_stg.UploadType.IO_STREAM)
boot_lu_name = pvm_u.sanitize_file_name_for_api(
instance.name, prefix=DiskType.BOOT + '_')
instance.name, prefix=disk_drv.DiskType.BOOT + '_')
LOG.info('SSP: Disk name is %s', boot_lu_name, instance=instance)
@ -257,59 +231,6 @@ class SSPDiskAdapter(object):
ret.append(n.vios_uuid)
return ret
def get_bootdisk_path(self, instance, vios_uuid):
"""Get the local path for an instance's boot disk.
:param instance: nova.objects.instance.Instance object owning the
requested disk.
:param vios_uuid: PowerVM UUID of the VIOS to search for mappings.
:return: Local path for instance's boot disk.
"""
vm_uuid = vm.get_pvm_uuid(instance)
match_func = self._disk_match_func(DiskType.BOOT, instance)
vios_wrap = pvm_vios.VIOS.get(self._adapter, uuid=vios_uuid,
xag=[pvm_const.XAG.VIO_SMAP])
maps = tsk_map.find_maps(vios_wrap.scsi_mappings,
client_lpar_id=vm_uuid, match_func=match_func)
if maps:
return maps[0].server_adapter.backing_dev_name
return None
def connect_instance_disk_to_mgmt(self, instance):
"""Connect an instance's boot disk to the management partition.
:param instance: The instance whose boot disk is to be mapped.
:return stg_elem: The storage element (LU, VDisk, etc.) that was mapped
:return vios: The EntryWrapper of the VIOS from which the mapping was
made.
:raise InstanceDiskMappingFailed: If the mapping could not be done.
"""
for stg_elem, vios in self._get_bootdisk_iter(instance):
msg_args = {'disk_name': stg_elem.name, 'vios_name': vios.name}
# Create a new mapping. NOTE: If there's an existing mapping on
# the other VIOS but not this one, we'll create a second mapping
# here. It would take an extreme sequence of events to get to that
# point, and the second mapping would be harmless anyway. The
# alternative would be always checking all VIOSes for existing
# mappings, which increases the response time of the common case by
# an entire GET of VIOS+VIO_SMAP.
LOG.debug("Mapping boot disk %(disk_name)s to the management "
"partition from Virtual I/O Server %(vios_name)s.",
msg_args, instance=instance)
try:
tsk_map.add_vscsi_mapping(self._host_uuid, vios, self.mp_uuid,
stg_elem)
# If that worked, we're done. add_vscsi_mapping logged.
return stg_elem, vios
except pvm_exc.Error:
LOG.exception("Failed to map boot disk %(disk_name)s to the "
"management partition from Virtual I/O Server "
"%(vios_name)s.", msg_args, instance=instance)
# Try the next hit, if available.
# We either didn't find the boot dev, or failed all attempts to map it.
raise exception.InstanceDiskMappingFailed(instance_name=instance.name)
def disconnect_disk_from_mgmt(self, vios_uuid, disk_name):
"""Disconnect a disk from the management partition.
@ -335,42 +256,3 @@ class SSPDiskAdapter(object):
"""
disk_name = SSPDiskAdapter._get_disk_name(disk_type, instance)
return tsk_map.gen_match_func(pvm_stg.LU, names=[disk_name])
@staticmethod
def _get_disk_name(disk_type, instance, short=False):
"""Generate a name for a virtual disk associated with an instance.
:param disk_type: One of the DiskType enum values.
:param instance: The instance for which the disk is to be created.
:param short: If True, the generated name will be limited to 15
characters (the limit for virtual disk). If False, it
will be limited by the API (79 characters currently).
:return: The sanitized file name for the disk.
"""
prefix = '%s_' % (disk_type[0] if short else disk_type)
base = ('%s_%s' % (instance.name[:8], instance.uuid[:4]) if short
else instance.name)
return pvm_u.sanitize_file_name_for_api(
base, prefix=prefix, max_len=pvm_const.MaxLen.VDISK_NAME if short
else pvm_const.MaxLen.FILENAME_DEFAULT)
def _get_bootdisk_iter(self, instance):
"""Return an iterator of (storage_elem, VIOS) tuples for the instance.
storage_elem is a pypowervm storage element wrapper associated with
the instance boot disk and VIOS is the wrapper of the Virtual I/O
server owning that storage element.
:param instance: nova.objects.instance.Instance object owning the
requested disk.
:return: Iterator of tuples of (storage_elem, VIOS).
"""
lpar_wrap = vm.get_instance_wrapper(self._adapter, instance)
match_func = self._disk_match_func(DiskType.BOOT, instance)
for vios_uuid in self._vios_uuids:
vios_wrap = pvm_vios.VIOS.get(
self._adapter, uuid=vios_uuid, xag=[pvm_const.XAG.VIO_SMAP])
for scsi_map in tsk_map.find_maps(
vios_wrap.scsi_mappings, client_lpar_id=lpar_wrap.id,
match_func=match_func):
yield scsi_map.backing_storage, vios_wrap