Infinidat: support for manage/unmanage API

This patch adds some missing features to the Infinidat driver:
- Volume manage and unmanage
- Snapshot manage and unmanage
- List manageable volumes and snapshots

Implements: blueprint infinidat-manage-unmanage-vol
Implements: blueprint infinidat-manage-unmanage-snap
Change-Id: I046804c125f0b57c5c789adc67c2b83dc57076cd
Signed-off-by: Alexander Deiter <adeiter@infinidat.com>
This commit is contained in:
Alexander Deiter 2022-07-07 12:10:26 +04:00
parent 2c25e2259e
commit 9cf599064f
4 changed files with 609 additions and 38 deletions

View File

@ -24,6 +24,7 @@ import ddt
from oslo_utils import units from oslo_utils import units
from cinder import exception from cinder import exception
from cinder.tests.unit import fake_constants as fake
from cinder.tests.unit import test from cinder.tests.unit import test
from cinder import version from cinder import version
from cinder.volume import configuration from cinder.volume import configuration
@ -49,12 +50,20 @@ TEST_TARGET_PORTAL3 = '{}:{}'.format(TEST_IP_ADDRESS3, TEST_ISCSI_TCP_PORT2)
TEST_TARGET_PORTAL4 = '{}:{}'.format(TEST_IP_ADDRESS4, TEST_ISCSI_TCP_PORT2) TEST_TARGET_PORTAL4 = '{}:{}'.format(TEST_IP_ADDRESS4, TEST_ISCSI_TCP_PORT2)
TEST_FC_PROTOCOL = 'fc' TEST_FC_PROTOCOL = 'fc'
TEST_ISCSI_PROTOCOL = 'iscsi' TEST_ISCSI_PROTOCOL = 'iscsi'
TEST_VOLUME_SOURCE_NAME = 'test-volume'
TEST_VOLUME_SOURCE_ID = 12345
TEST_VOLUME_METADATA = {'cinder_id': fake.VOLUME_ID}
TEST_SNAPSHOT_SOURCE_NAME = 'test-snapshot'
TEST_SNAPSHOT_SOURCE_ID = 67890
TEST_SNAPSHOT_METADATA = {'cinder_id': fake.SNAPSHOT_ID}
test_volume = mock.Mock(id=1, size=1, volume_type_id=1) test_volume = mock.Mock(id=fake.VOLUME_ID, size=1,
test_snapshot = mock.Mock(id=2, volume=test_volume, volume_id='1') volume_type_id=fake.VOLUME_TYPE_ID)
test_clone = mock.Mock(id=3, size=1) test_snapshot = mock.Mock(id=fake.SNAPSHOT_ID, volume=test_volume,
test_group = mock.Mock(id=4) volume_id=test_volume.id)
test_snapgroup = mock.Mock(id=5, group=test_group) test_clone = mock.Mock(id=fake.VOLUME4_ID, size=1)
test_group = mock.Mock(id=fake.GROUP_ID)
test_snapgroup = mock.Mock(id=fake.GROUP_SNAPSHOT_ID, group=test_group)
test_connector = dict(wwpns=[TEST_WWN_1], test_connector = dict(wwpns=[TEST_WWN_1],
initiator=TEST_INITIATOR_IQN) initiator=TEST_INITIATOR_IQN)
@ -120,9 +129,17 @@ class InfiniboxDriverTestCaseBase(test.TestCase):
def _infinibox_mock(self): def _infinibox_mock(self):
result = mock.Mock() result = mock.Mock()
self._mock_volume = mock.Mock() self._mock_volume = mock.Mock()
self._mock_new_volume = mock.Mock()
self._mock_volume.get_id.return_value = TEST_VOLUME_SOURCE_ID
self._mock_volume.get_name.return_value = TEST_VOLUME_SOURCE_NAME
self._mock_volume.get_type.return_value = 'MASTER'
self._mock_volume.get_pool_name.return_value = (
self.configuration.infinidat_pool_name)
self._mock_volume.get_size.return_value = 1 * units.Gi self._mock_volume.get_size.return_value = 1 * units.Gi
self._mock_volume.has_children.return_value = False self._mock_volume.has_children.return_value = False
self._mock_volume.get_qos_policy.return_value = None
self._mock_volume.get_logical_units.return_value = [] self._mock_volume.get_logical_units.return_value = []
self._mock_volume.get_all_metadata.return_value = {}
self._mock_volume.create_snapshot.return_value = self._mock_volume self._mock_volume.create_snapshot.return_value = self._mock_volume
self._mock_snapshot = mock.Mock() self._mock_snapshot = mock.Mock()
self._mock_host = mock.Mock() self._mock_host = mock.Mock()
@ -131,6 +148,7 @@ class InfiniboxDriverTestCaseBase(test.TestCase):
self._mock_pool = mock.Mock() self._mock_pool = mock.Mock()
self._mock_pool.get_free_physical_capacity.return_value = units.Gi self._mock_pool.get_free_physical_capacity.return_value = units.Gi
self._mock_pool.get_physical_capacity.return_value = units.Gi self._mock_pool.get_physical_capacity.return_value = units.Gi
self._mock_pool.get_volumes.return_value = [self._mock_volume]
self._mock_name_space1 = mock.Mock() self._mock_name_space1 = mock.Mock()
self._mock_name_space2 = mock.Mock() self._mock_name_space2 = mock.Mock()
self._mock_name_space1.get_ips.return_value = [ self._mock_name_space1.get_ips.return_value = [
@ -218,7 +236,7 @@ class InfiniboxDriverTestCase(InfiniboxDriverTestCaseBase):
def test_initialize_connection_volume_doesnt_exist(self): def test_initialize_connection_volume_doesnt_exist(self):
self._system.volumes.safe_get.return_value = None self._system.volumes.safe_get.return_value = None
self.assertRaises(exception.InvalidVolume, self.assertRaises(exception.VolumeNotFound,
self.driver.initialize_connection, self.driver.initialize_connection,
test_volume, test_connector) test_volume, test_connector)
@ -253,7 +271,7 @@ class InfiniboxDriverTestCase(InfiniboxDriverTestCaseBase):
def test_terminate_connection_volume_doesnt_exist(self): def test_terminate_connection_volume_doesnt_exist(self):
self._system.volumes.safe_get.return_value = None self._system.volumes.safe_get.return_value = None
self.assertRaises(exception.InvalidVolume, self.assertRaises(exception.VolumeNotFound,
self.driver.terminate_connection, self.driver.terminate_connection,
test_volume, test_connector) test_volume, test_connector)
@ -363,7 +381,7 @@ class InfiniboxDriverTestCase(InfiniboxDriverTestCaseBase):
def test_create_snapshot_volume_doesnt_exist(self): def test_create_snapshot_volume_doesnt_exist(self):
self._system.volumes.safe_get.return_value = None self._system.volumes.safe_get.return_value = None
self.assertRaises(exception.InvalidVolume, self.assertRaises(exception.VolumeNotFound,
self.driver.create_snapshot, test_snapshot) self.driver.create_snapshot, test_snapshot)
def test_create_snapshot_api_fail(self): def test_create_snapshot_api_fail(self):
@ -381,7 +399,7 @@ class InfiniboxDriverTestCase(InfiniboxDriverTestCaseBase):
def test_create_volume_from_snapshot_doesnt_exist(self): def test_create_volume_from_snapshot_doesnt_exist(self):
self._system.volumes.safe_get.return_value = None self._system.volumes.safe_get.return_value = None
self.assertRaises(exception.InvalidSnapshot, self.assertRaises(exception.SnapshotNotFound,
self.driver.create_volume_from_snapshot, self.driver.create_volume_from_snapshot,
test_clone, test_snapshot) test_clone, test_snapshot)
@ -624,6 +642,221 @@ class InfiniboxDriverTestCase(InfiniboxDriverTestCaseBase):
delta = (snapshot_size - volume_size) * units.Gi delta = (snapshot_size - volume_size) * units.Gi
self._mock_volume.resize.assert_called_with(delta) self._mock_volume.resize.assert_called_with(delta)
@mock.patch('cinder.volume.volume_types.get_volume_type_qos_specs')
def test_manage_existing_by_source_name(self, *mocks):
existing_ref = {'source-name': TEST_VOLUME_SOURCE_NAME}
self.driver.manage_existing(test_volume, existing_ref)
@mock.patch('cinder.volume.volume_types.get_volume_type_qos_specs')
def test_manage_existing_by_source_id(self, *mocks):
existing_ref = {'source-id': TEST_VOLUME_SOURCE_ID}
self.driver.manage_existing(test_volume, existing_ref)
@mock.patch('cinder.volume.volume_types.get_volume_type_qos_specs')
def test_manage_existing_by_invalid_source(self, *mocks):
existing_ref = {'source-path': None}
self.assertRaises(exception.ManageExistingInvalidReference,
self.driver.manage_existing,
test_volume, existing_ref)
@mock.patch('cinder.volume.volume_types.get_volume_type_qos_specs')
@mock.patch('cinder.volume.volume_utils.check_already_managed_volume',
return_value=False)
def test_manage_existing_not_managed(self, *mocks):
self._mock_volume.get_all_metadata.return_value = (
TEST_VOLUME_METADATA)
existing_ref = {'source-name': TEST_VOLUME_SOURCE_NAME}
self.driver.manage_existing(test_volume, existing_ref)
@mock.patch('cinder.volume.volume_types.get_volume_type_qos_specs')
@mock.patch('cinder.volume.volume_utils.check_already_managed_volume',
return_value=True)
def test_manage_existing_already_managed(self, *mocks):
self._mock_volume.get_all_metadata.return_value = (
TEST_VOLUME_METADATA)
existing_ref = {'source-name': TEST_VOLUME_SOURCE_NAME}
self.assertRaises(exception.ManageExistingAlreadyManaged,
self.driver.manage_existing,
test_volume, existing_ref)
@mock.patch('cinder.volume.volume_types.get_volume_type_qos_specs')
def test_manage_existing_invalid_pool(self, *mocks):
existing_ref = {'source-name': TEST_VOLUME_SOURCE_NAME}
self._mock_volume.get_pool_name.return_value = 'invalid'
self.assertRaises(exception.InvalidConfigurationValue,
self.driver.manage_existing,
test_volume, existing_ref)
def test_manage_existing_get_size(self):
existing_ref = {'source-name': TEST_VOLUME_SOURCE_NAME}
size = self.driver.manage_existing_get_size(test_volume, existing_ref)
self.assertEqual(test_volume.size, size)
def test_get_manageable_volumes(self):
cinder_volumes = [test_volume]
self._mock_volume.is_snapshot.return_value = False
self._mock_volume.get_all_metadata.return_value = {
'cinder_id': fake.VOLUME2_ID
}
self.driver.get_manageable_volumes(cinder_volumes, None,
1, 0, [], [])
def test_get_manageable_volumes_already_managed(self):
cinder_volumes = [test_volume]
self._mock_volume.get_id.return_value = TEST_VOLUME_SOURCE_ID
self._mock_volume.get_all_metadata.return_value = (
TEST_VOLUME_METADATA)
self._mock_volume.is_snapshot.return_value = False
self.driver.get_manageable_volumes(cinder_volumes, None,
1, 0, [], [])
def test_get_manageable_volumes_but_snapshots(self):
cinder_volumes = [test_volume]
self._mock_volume.is_snapshot.return_value = True
self.driver.get_manageable_volumes(cinder_volumes, None,
1, 0, [], [])
def test_get_manageable_volumes_has_mappings(self):
cinder_volumes = [test_volume]
self._mock_volume.is_snapshot.return_value = False
self._mock_volume.get_all_metadata.return_value = {
'cinder_id': fake.VOLUME2_ID
}
lun = mock.Mock()
self._mock_volume.get_logical_units.return_value = [lun]
self.driver.get_manageable_volumes(cinder_volumes, None,
1, 0, [], [])
def test_get_manageable_volumes_has_snapshots(self):
cinder_volumes = [test_volume]
self._mock_volume.is_snapshot.return_value = False
self._mock_volume.has_children.return_value = True
self._mock_volume.get_all_metadata.return_value = {
'cinder_id': fake.VOLUME2_ID
}
self.driver.get_manageable_volumes(cinder_volumes, None,
1, 0, [], [])
def test_unmanage(self):
self.driver.unmanage(test_volume)
@mock.patch('cinder.objects.Snapshot.exists', return_value=True)
def test__check_already_managed_snapshot(self, *mocks):
self.driver._check_already_managed_snapshot(test_snapshot.id)
@mock.patch('cinder.volume.volume_types.get_volume_type_qos_specs')
def test_manage_existing_snapshot_by_source_name(self, *mocks):
existing_ref = {'source-name': TEST_SNAPSHOT_SOURCE_NAME}
self.driver.manage_existing_snapshot(test_snapshot, existing_ref)
@mock.patch('cinder.volume.volume_types.get_volume_type_qos_specs')
def test_manage_existing_snapshot_by_source_id(self, *mocks):
existing_ref = {'source-id': TEST_SNAPSHOT_SOURCE_ID}
self.driver.manage_existing_snapshot(test_snapshot, existing_ref)
@mock.patch('cinder.volume.volume_types.get_volume_type_qos_specs')
def test_manage_existing_snapshot_but_volume(self, *mocks):
existing_ref = {'source-id': TEST_SNAPSHOT_SOURCE_ID}
self._mock_volume.is_snapshot.return_value = False
self.assertRaises(exception.InvalidSnapshot,
self.driver.manage_existing_snapshot,
test_snapshot, existing_ref)
@mock.patch('cinder.volume.volume_types.get_volume_type_qos_specs')
def test_manage_existing_snapshot_by_invalid_source(self, *mocks):
existing_ref = {'source-path': None}
self.assertRaises(exception.ManageExistingInvalidReference,
self.driver.manage_existing_snapshot,
test_snapshot, existing_ref)
@mock.patch('cinder.volume.volume_types.get_volume_type_qos_specs')
def test_manage_existing_snapshot_by_non_cinder_id(self, *mocks):
self._mock_volume.get_all_metadata.return_value = {'cinder_id': 'x'}
existing_ref = {'source-id': TEST_SNAPSHOT_SOURCE_ID}
self.driver.manage_existing_snapshot(test_snapshot, existing_ref)
@mock.patch('cinder.volume.volume_types.get_volume_type_qos_specs')
@mock.patch('cinder.volume.drivers.infinidat.InfiniboxVolumeDriver.'
'_check_already_managed_snapshot', return_value=False)
def test_manage_existing_snapshot_not_managed(self, *mocks):
self._mock_volume.get_all_metadata.return_value = (
TEST_SNAPSHOT_METADATA)
existing_ref = {'source-name': TEST_SNAPSHOT_SOURCE_NAME}
self.driver.manage_existing(test_snapshot, existing_ref)
@mock.patch('cinder.volume.volume_types.get_volume_type_qos_specs')
@mock.patch('cinder.volume.drivers.infinidat.InfiniboxVolumeDriver.'
'_check_already_managed_snapshot', return_value=True)
def test_manage_existing_snapshot_already_managed(self, *mocks):
self._mock_volume.get_all_metadata.return_value = (
TEST_SNAPSHOT_METADATA)
existing_ref = {'source-name': TEST_SNAPSHOT_SOURCE_NAME}
self.assertRaises(exception.ManageExistingAlreadyManaged,
self.driver.manage_existing_snapshot,
test_snapshot, existing_ref)
@mock.patch('cinder.volume.volume_types.get_volume_type_qos_specs')
def test_manage_existing_snapshot_invalid_pool(self, *mocks):
existing_ref = {'source-name': TEST_SNAPSHOT_SOURCE_NAME}
self._mock_volume.get_pool_name.return_value = 'invalid'
self.assertRaises(exception.InvalidConfigurationValue,
self.driver.manage_existing_snapshot,
test_snapshot, existing_ref)
def test_manage_existing_snapshot_get_size(self):
existing_ref = {'source-name': TEST_SNAPSHOT_SOURCE_NAME}
size = self.driver.manage_existing_snapshot_get_size(test_volume,
existing_ref)
self.assertEqual(test_snapshot.volume.size, size)
def test_get_manageable_snapshots(self):
cinder_snapshots = [test_snapshot]
self._mock_volume.is_snapshot.return_value = True
self._mock_volume.get_all_metadata.return_value = {
'cinder_id': fake.SNAPSHOT2_ID
}
self.driver.get_manageable_snapshots(cinder_snapshots,
None, 1, 0, [], [])
def test_get_manageable_snapshots_already_managed(self):
cinder_snapshots = [test_snapshot]
self._mock_volume.get_id.return_value = TEST_SNAPSHOT_SOURCE_ID
self._mock_volume.get_all_metadata.return_value = (
TEST_SNAPSHOT_METADATA)
self._mock_volume.is_snapshot.return_value = True
self.driver.get_manageable_snapshots(cinder_snapshots,
None, 1, 0, [], [])
def test_get_manageable_snapshots_but_volumes(self):
cinder_snapshots = [test_snapshot]
self._mock_volume.is_snapshot.return_value = False
self.driver.get_manageable_snapshots(cinder_snapshots,
None, 1, 0, [], [])
def test_get_manageable_snapshots_has_mappings(self):
cinder_snapshots = [test_snapshot]
self._mock_volume.is_snapshot.return_value = True
self._mock_volume.get_all_metadata.return_value = {
'cinder_id': fake.SNAPSHOT2_ID
}
lun = mock.Mock()
self._mock_volume.get_logical_units.return_value = [lun]
self.driver.get_manageable_snapshots(cinder_snapshots,
None, 1, 0, [], [])
def test_get_manageable_snapshots_has_clones(self):
cinder_snapshots = [test_snapshot]
self._mock_volume.is_snapshot.return_value = True
self._mock_volume.has_children.return_value = True
self._mock_volume.get_all_metadata.return_value = {
'cinder_id': fake.SNAPSHOT2_ID
}
self.driver.get_manageable_snapshots(cinder_snapshots,
None, 1, 0, [], [])
def test_unmanage_snapshot(self):
self.driver.unmanage_snapshot(test_snapshot)
class InfiniboxDriverTestCaseFC(InfiniboxDriverTestCaseBase): class InfiniboxDriverTestCaseFC(InfiniboxDriverTestCaseBase):
def test_initialize_connection_multiple_wwpns(self): def test_initialize_connection_multiple_wwpns(self):

View File

@ -16,19 +16,23 @@
from contextlib import contextmanager from contextlib import contextmanager
import functools import functools
import math
import platform import platform
import socket import socket
from unittest import mock from unittest import mock
import uuid
from oslo_config import cfg from oslo_config import cfg
from oslo_log import log as logging from oslo_log import log as logging
from oslo_utils import units from oslo_utils import units
from cinder.common import constants from cinder.common import constants
from cinder import context as cinder_context
from cinder import coordination from cinder import coordination
from cinder import exception from cinder import exception
from cinder.i18n import _ from cinder.i18n import _
from cinder import interface from cinder import interface
from cinder import objects
from cinder.objects import fields from cinder.objects import fields
from cinder import version from cinder import version
from cinder.volume import configuration from cinder.volume import configuration
@ -119,10 +123,11 @@ class InfiniboxVolumeDriver(san.SanISCSIDriver):
1.6 - added support for volume multi-attach 1.6 - added support for volume multi-attach
1.7 - fixed iSCSI to return all portals 1.7 - fixed iSCSI to return all portals
1.8 - added revert to snapshot 1.8 - added revert to snapshot
1.9 - added manage/unmanage/manageable-list volume/snapshot
""" """
VERSION = '1.8' VERSION = '1.9'
# ThirdPartySystems wiki page # ThirdPartySystems wiki page
CI_WIKI_NAME = "INFINIDAT_CI" CI_WIKI_NAME = "INFINIDAT_CI"
@ -223,21 +228,41 @@ class InfiniboxVolumeDriver(san.SanISCSIDriver):
"host.created_by": _INFINIDAT_CINDER_IDENTIFIER} "host.created_by": _INFINIDAT_CINDER_IDENTIFIER}
infinidat_object.set_metadata_from_dict(data) infinidat_object.set_metadata_from_dict(data)
def _get_infinidat_dataset_by_ref(self, existing_ref):
if 'source-id' in existing_ref:
kwargs = dict(id=existing_ref['source-id'])
elif 'source-name' in existing_ref:
kwargs = dict(name=existing_ref['source-name'])
else:
reason = _('dataset reference must contain '
'source-id or source-name key')
raise exception.ManageExistingInvalidReference(
existing_ref=existing_ref, reason=reason)
return self._system.volumes.safe_get(**kwargs)
def _get_infinidat_volume_by_ref(self, existing_ref):
infinidat_volume = self._get_infinidat_dataset_by_ref(existing_ref)
if infinidat_volume is None:
raise exception.VolumeNotFound(volume_id=existing_ref)
return infinidat_volume
def _get_infinidat_snapshot_by_ref(self, existing_ref):
infinidat_snapshot = self._get_infinidat_dataset_by_ref(existing_ref)
if infinidat_snapshot is None:
raise exception.SnapshotNotFound(snapshot_id=existing_ref)
if not infinidat_snapshot.is_snapshot():
reason = (_('reference %(existing_ref)s is a volume')
% {'existing_ref': existing_ref})
raise exception.InvalidSnapshot(reason=reason)
return infinidat_snapshot
def _get_infinidat_volume_by_name(self, name): def _get_infinidat_volume_by_name(self, name):
volume = self._system.volumes.safe_get(name=name) ref = {'source-name': name}
if volume is None: return self._get_infinidat_volume_by_ref(ref)
msg = _('Volume "%s" not found') % name
LOG.error(msg)
raise exception.InvalidVolume(reason=msg)
return volume
def _get_infinidat_snapshot_by_name(self, name): def _get_infinidat_snapshot_by_name(self, name):
snapshot = self._system.volumes.safe_get(name=name) ref = {'source-name': name}
if snapshot is None: return self._get_infinidat_snapshot_by_ref(ref)
msg = _('Snapshot "%s" not found') % name
LOG.error(msg)
raise exception.InvalidSnapshot(reason=msg)
return snapshot
def _get_infinidat_volume(self, cinder_volume): def _get_infinidat_volume(self, cinder_volume):
volume_name = self._make_volume_name(cinder_volume) volume_name = self._make_volume_name(cinder_volume)
@ -260,9 +285,7 @@ class InfiniboxVolumeDriver(san.SanISCSIDriver):
group_name = self._make_cg_name(cinder_group) group_name = self._make_cg_name(cinder_group)
infinidat_cg = self._system.cons_groups.safe_get(name=group_name) infinidat_cg = self._system.cons_groups.safe_get(name=group_name)
if infinidat_cg is None: if infinidat_cg is None:
msg = _('Consistency group "%s" not found') % group_name raise exception.GroupNotFound(group_id=group_name)
LOG.error(msg)
raise exception.InvalidGroup(message=msg)
return infinidat_cg return infinidat_cg
def _get_or_create_host(self, port): def _get_or_create_host(self, port):
@ -338,8 +361,7 @@ class InfiniboxVolumeDriver(san.SanISCSIDriver):
yield str(port.get_wwpn()) yield str(port.get_wwpn())
def _initialize_connection_fc(self, volume, connector): def _initialize_connection_fc(self, volume, connector):
volume_name = self._make_volume_name(volume) infinidat_volume = self._get_infinidat_volume(volume)
infinidat_volume = self._get_infinidat_volume_by_name(volume_name)
ports = [wwn.WWN(wwpn) for wwpn in connector['wwpns']] ports = [wwn.WWN(wwpn) for wwpn in connector['wwpns']]
for port in ports: for port in ports:
infinidat_host = self._get_or_create_host(port) infinidat_host = self._get_or_create_host(port)
@ -380,8 +402,7 @@ class InfiniboxVolumeDriver(san.SanISCSIDriver):
raise exception.VolumeDriverException(message=msg) raise exception.VolumeDriverException(message=msg)
def _initialize_connection_iscsi(self, volume, connector): def _initialize_connection_iscsi(self, volume, connector):
volume_name = self._make_volume_name(volume) infinidat_volume = self._get_infinidat_volume(volume)
infinidat_volume = self._get_infinidat_volume_by_name(volume_name)
port = iqn.IQN(connector['initiator']) port = iqn.IQN(connector['initiator'])
infinidat_host = self._get_or_create_host(port) infinidat_host = self._get_or_create_host(port)
if self.configuration.use_chap_auth: if self.configuration.use_chap_auth:
@ -546,14 +567,13 @@ class InfiniboxVolumeDriver(san.SanISCSIDriver):
@infinisdk_to_cinder_exceptions @infinisdk_to_cinder_exceptions
def delete_volume(self, volume): def delete_volume(self, volume):
"""Delete a volume from the backend.""" """Delete a volume from the backend."""
volume_name = self._make_volume_name(volume)
try: try:
infinidat_volume = self._get_infinidat_volume_by_name(volume_name) infinidat_volume = self._get_infinidat_volume(volume)
except exception.InvalidVolume: except exception.VolumeNotFound:
return # volume not found return
if infinidat_volume.has_children(): if infinidat_volume.has_children():
# can't delete a volume that has a live snapshot # can't delete a volume that has a live snapshot
raise exception.VolumeIsBusy(volume_name=volume_name) raise exception.VolumeIsBusy(volume_name=volume.name)
infinidat_volume.safe_delete() infinidat_volume.safe_delete()
@infinisdk_to_cinder_exceptions @infinisdk_to_cinder_exceptions
@ -654,8 +674,8 @@ class InfiniboxVolumeDriver(san.SanISCSIDriver):
"""Deletes a snapshot.""" """Deletes a snapshot."""
try: try:
snapshot = self._get_infinidat_snapshot(snapshot) snapshot = self._get_infinidat_snapshot(snapshot)
except exception.InvalidSnapshot: except exception.SnapshotNotFound:
return # snapshot not found return
snapshot.safe_delete() snapshot.safe_delete()
def _asssert_volume_not_mapped(self, volume): def _asssert_volume_not_mapped(self, volume):
@ -749,8 +769,8 @@ class InfiniboxVolumeDriver(san.SanISCSIDriver):
raise NotImplementedError() raise NotImplementedError()
try: try:
infinidat_cg = self._get_infinidat_cg(group) infinidat_cg = self._get_infinidat_cg(group)
except exception.InvalidGroup: except exception.GroupNotFound:
pass # group not found pass
else: else:
infinidat_cg.safe_delete() infinidat_cg.safe_delete()
for volume in volumes: for volume in volumes:
@ -859,3 +879,313 @@ class InfiniboxVolumeDriver(san.SanISCSIDriver):
snapshot_size = snapshot.volume.size * capacity.GiB snapshot_size = snapshot.volume.size * capacity.GiB
if volume_size < snapshot_size: if volume_size < snapshot_size:
self.extend_volume(volume, snapshot.volume.size) self.extend_volume(volume, snapshot.volume.size)
@infinisdk_to_cinder_exceptions
def manage_existing(self, volume, existing_ref):
"""Manage an existing Infinidat volume.
Checks if the volume is already managed.
Renames the Infinidat volume to match the expected name.
Updates QoS and metadata.
:param volume: Cinder volume to manage
:param existing_ref: dictionary of the forms:
{'source-name': 'Infinidat volume name'} or
{'source-id': 'Infinidat volume serial number'}
"""
infinidat_volume = self._get_infinidat_volume_by_ref(existing_ref)
infinidat_metadata = infinidat_volume.get_all_metadata()
if 'cinder_id' in infinidat_metadata:
cinder_id = infinidat_metadata['cinder_id']
if volume_utils.check_already_managed_volume(cinder_id):
raise exception.ManageExistingAlreadyManaged(
volume_ref=cinder_id)
infinidat_pool = infinidat_volume.get_pool_name()
if infinidat_pool != self.configuration.infinidat_pool_name:
message = (_('unexpected pool name %(infinidat_pool)s')
% {'infinidat_pool': infinidat_pool})
raise exception.InvalidConfigurationValue(message=message)
cinder_name = self._make_volume_name(volume)
infinidat_volume.update_name(cinder_name)
self._set_qos(volume, infinidat_volume)
self._set_cinder_object_metadata(infinidat_volume, volume)
@infinisdk_to_cinder_exceptions
def manage_existing_get_size(self, volume, existing_ref):
"""Return size of an existing Infinidat volume.
When calculating the size, round up to the next GB.
:param volume: Cinder volume to manage
:param existing_ref: dictionary of the forms:
{'source-name': 'Infinidat volume name'} or
{'source-id': 'Infinidat volume serial number'}
:returns size: Volume size in GiB (integer)
"""
infinidat_volume = self._get_infinidat_volume_by_ref(existing_ref)
return int(math.ceil(infinidat_volume.get_size() / capacity.GiB))
@infinisdk_to_cinder_exceptions
def get_manageable_volumes(self, cinder_volumes, marker, limit, offset,
sort_keys, sort_dirs):
"""List volumes on the Infinidat backend available for management.
Returns a list of dictionaries, each specifying a volume on the
Infinidat backend, with the following keys:
- reference (dictionary): The reference for a volume, which can be
passed to "manage_existing". Each reference contains keys:
Infinidat volume name and Infinidat volume serial number.
- size (int): The size of the volume according to the Infinidat
storage backend, rounded up to the nearest GB.
- safe_to_manage (boolean): Whether or not this volume is safe to
manage according to the storage backend. For example, is the volume
already managed, in use, has snapshots or active mappings.
- reason_not_safe (string): If safe_to_manage is False, the reason why.
- cinder_id (string): If already managed, provide the Cinder ID.
- extra_info (string): Extra information (pool name, volume type,
QoS and metadata) to return to the user.
:param cinder_volumes: A list of volumes in this host that Cinder
currently manages, used to determine if
a volume is manageable or not.
:param marker: The last item of the previous page; we return the
next results after this value (after sorting)
:param limit: Maximum number of items to return
:param offset: Number of items to skip after marker
:param sort_keys: List of keys to sort results by (valid keys are
'identifier' and 'size')
:param sort_dirs: List of directions to sort by, corresponding to
sort_keys (valid directions are 'asc' and 'desc')
"""
manageable_volumes = []
cinder_ids = [cinder_volume.id for cinder_volume in cinder_volumes]
infinidat_pool = self._get_infinidat_pool()
infinidat_volumes = infinidat_pool.get_volumes()
for infinidat_volume in infinidat_volumes:
if infinidat_volume.is_snapshot():
continue
safe_to_manage = False
reason_not_safe = None
volume_id = infinidat_volume.get_id()
volume_name = infinidat_volume.get_name()
volume_size = infinidat_volume.get_size()
volume_type = infinidat_volume.get_type()
volume_pool = infinidat_volume.get_pool_name()
volume_qos = infinidat_volume.get_qos_policy()
volume_meta = infinidat_volume.get_all_metadata()
cinder_id = volume_meta.get('cinder_id')
volume_luns = infinidat_volume.get_logical_units()
if cinder_id and cinder_id in cinder_ids:
reason_not_safe = _('volume already managed')
elif volume_luns:
reason_not_safe = _('volume has mappings')
elif infinidat_volume.has_children():
reason_not_safe = _('volume has snapshots')
else:
safe_to_manage = True
reference = {
'source-name': volume_name,
'source-id': str(volume_id)
}
extra_info = {
'pool': volume_pool,
'type': volume_type,
'qos': str(volume_qos),
'meta': str(volume_meta)
}
manageable_volume = {
'reference': reference,
'size': int(math.ceil(volume_size / capacity.GiB)),
'safe_to_manage': safe_to_manage,
'reason_not_safe': reason_not_safe,
'cinder_id': cinder_id,
'extra_info': extra_info
}
manageable_volumes.append(manageable_volume)
return volume_utils.paginate_entries_list(
manageable_volumes, marker, limit,
offset, sort_keys, sort_dirs)
@infinisdk_to_cinder_exceptions
def unmanage(self, volume):
"""Removes the specified volume from Cinder management.
Does not delete the underlying backend storage object.
For most drivers, this will not need to do anything. However, some
drivers might use this call as an opportunity to clean up any
Cinder-specific configuration that they have associated with the
backend storage object.
:param volume: Cinder volume to unmanage
"""
infinidat_volume = self._get_infinidat_volume(volume)
infinidat_volume.clear_metadata()
def _check_already_managed_snapshot(self, snapshot_id):
"""Check cinder db for already managed snapshot.
:param snapshot_id snapshot id parameter
:returns: bool -- return True, if db entry with specified
snapshot id exists, otherwise return False
"""
try:
uuid.UUID(snapshot_id, version=4)
except ValueError:
return False
ctxt = cinder_context.get_admin_context()
return objects.Snapshot.exists(ctxt, snapshot_id)
@infinisdk_to_cinder_exceptions
def manage_existing_snapshot(self, snapshot, existing_ref):
"""Manage an existing Infinidat snapshot.
Checks if the snapshot is already managed.
Renames the Infinidat snapshot to match the expected name.
Updates QoS and metadata.
:param snapshot: Cinder snapshot to manage
:param existing_ref: dictionary of the forms:
{'source-name': 'Infinidat snapshot name'} or
{'source-id': 'Infinidat snapshot serial number'}
"""
infinidat_snapshot = self._get_infinidat_snapshot_by_ref(existing_ref)
infinidat_metadata = infinidat_snapshot.get_all_metadata()
if 'cinder_id' in infinidat_metadata:
cinder_id = infinidat_metadata['cinder_id']
if self._check_already_managed_snapshot(cinder_id):
raise exception.ManageExistingAlreadyManaged(
volume_ref=cinder_id)
infinidat_pool = infinidat_snapshot.get_pool_name()
if infinidat_pool != self.configuration.infinidat_pool_name:
message = (_('unexpected pool name %(infinidat_pool)s')
% {'infinidat_pool': infinidat_pool})
raise exception.InvalidConfigurationValue(message=message)
cinder_name = self._make_snapshot_name(snapshot)
infinidat_snapshot.update_name(cinder_name)
self._set_qos(snapshot, infinidat_snapshot)
self._set_cinder_object_metadata(infinidat_snapshot, snapshot)
@infinisdk_to_cinder_exceptions
def manage_existing_snapshot_get_size(self, snapshot, existing_ref):
"""Return size of an existing Infinidat snapshot.
When calculating the size, round up to the next GB.
:param snapshot: Cinder snapshot to manage
:param existing_ref: dictionary of the forms:
{'source-name': 'Infinidat snapshot name'} or
{'source-id': 'Infinidat snapshot serial number'}
:returns size: Snapshot size in GiB (integer)
"""
infinidat_snapshot = self._get_infinidat_snapshot_by_ref(existing_ref)
return int(math.ceil(infinidat_snapshot.get_size() / capacity.GiB))
@infinisdk_to_cinder_exceptions
def get_manageable_snapshots(self, cinder_snapshots, marker, limit, offset,
sort_keys, sort_dirs):
"""List snapshots on the Infinidat backend available for management.
Returns a list of dictionaries, each specifying a snapshot on the
Infinidat backend, with the following keys:
- reference (dictionary): The reference for a snapshot, which can be
passed to "manage_existing_snapshot". Each reference contains keys:
Infinidat snapshot name and Infinidat snapshot serial number.
- size (int): The size of the snapshot according to the Infinidat
storage backend, rounded up to the nearest GB.
- safe_to_manage (boolean): Whether or not this snapshot is safe to
manage according to the storage backend. For example, is the snapshot
already managed, has clones or active mappings.
- reason_not_safe (string): If safe_to_manage is False, the reason why.
- cinder_id (string): If already managed, provide the Cinder ID.
- extra_info (string): Extra information (pool name, snapshot type,
QoS and metadata) to return to the user.
- source_reference (string): Similar to "reference", but for the
snapshot's source volume. The source reference contains two keys:
Infinidat volume name and Infinidat volume serial number.
:param cinder_snapshots: A list of snapshots in this host that Cinder
currently manages, used to determine if
a snapshot is manageable or not.
:param marker: The last item of the previous page; we return the
next results after this value (after sorting)
:param limit: Maximum number of items to return
:param offset: Number of items to skip after marker
:param sort_keys: List of keys to sort results by (valid keys are
'identifier' and 'size')
:param sort_dirs: List of directions to sort by, corresponding to
sort_keys (valid directions are 'asc' and 'desc')
"""
manageable_snapshots = []
cinder_ids = [cinder_snapshot.id for cinder_snapshot
in cinder_snapshots]
infinidat_pool = self._get_infinidat_pool()
infinidat_snapshots = infinidat_pool.get_volumes()
for infinidat_snapshot in infinidat_snapshots:
if not infinidat_snapshot.is_snapshot():
continue
safe_to_manage = False
reason_not_safe = None
parent = infinidat_snapshot.get_parent()
parent_id = parent.get_id()
parent_name = parent.get_name()
snapshot_id = infinidat_snapshot.get_id()
snapshot_name = infinidat_snapshot.get_name()
snapshot_size = infinidat_snapshot.get_size()
snapshot_type = infinidat_snapshot.get_type()
snapshot_pool = infinidat_snapshot.get_pool_name()
snapshot_qos = infinidat_snapshot.get_qos_policy()
snapshot_meta = infinidat_snapshot.get_all_metadata()
cinder_id = snapshot_meta.get('cinder_id')
snapshot_luns = infinidat_snapshot.get_logical_units()
if cinder_id and cinder_id in cinder_ids:
reason_not_safe = _('snapshot already managed')
elif snapshot_luns:
reason_not_safe = _('snapshot has mappings')
elif infinidat_snapshot.has_children():
reason_not_safe = _('snapshot has clones')
else:
safe_to_manage = True
reference = {
'source-name': snapshot_name,
'source-id': str(snapshot_id)
}
source_reference = {
'source-name': parent_name,
'source-id': str(parent_id)
}
extra_info = {
'pool': snapshot_pool,
'type': snapshot_type,
'qos': str(snapshot_qos),
'meta': str(snapshot_meta)
}
manageable_snapshot = {
'reference': reference,
'size': int(math.ceil(snapshot_size / capacity.GiB)),
'safe_to_manage': safe_to_manage,
'reason_not_safe': reason_not_safe,
'cinder_id': cinder_id,
'extra_info': extra_info,
'source_reference': source_reference
}
manageable_snapshots.append(manageable_snapshot)
return volume_utils.paginate_entries_list(
manageable_snapshots, marker, limit,
offset, sort_keys, sort_dirs)
@infinisdk_to_cinder_exceptions
def unmanage_snapshot(self, snapshot):
"""Removes the specified snapshot from Cinder management.
Does not delete the underlying backend storage object.
For most drivers, this will not need to do anything. However, some
drivers might use this call as an opportunity to clean up any
Cinder-specific configuration that they have associated with the
backend storage object.
:param snapshot: Cinder volume snapshot to unmanage
"""
infinidat_snapshot = self._get_infinidat_snapshot(snapshot)
infinidat_snapshot.clear_metadata()

View File

@ -23,6 +23,8 @@ Supported operations
* Create consistency group from consistency group or consistency group * Create consistency group from consistency group or consistency group
snapshot. snapshot.
* Revert a volume to a snapshot. * Revert a volume to a snapshot.
* Manage and unmanage volumes and snapshots.
* List manageable volumes and snapshots.
External package installation External package installation
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -0,0 +1,6 @@
---
features:
- |
Infinidat driver: Added support to manage and unmanage volumes and
snapshots. Also added the functionality to list the manageable
volumes and snapshots.