ScaleIO Driver: get manageable volumes
Implementation to allow listing of manageable volumes Change-Id: I76c62034bd45d00eb0e10d18f2ed21f08a3e3d10
This commit is contained in:
parent
34eefbe834
commit
c129e80cb0
@ -0,0 +1,191 @@
|
|||||||
|
# Copyright (C) 2017 Dell Inc. or its subsidiaries.
|
||||||
|
# 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 copy import deepcopy
|
||||||
|
|
||||||
|
import ddt
|
||||||
|
import mock
|
||||||
|
|
||||||
|
from cinder.tests.unit import fake_volume
|
||||||
|
from cinder.tests.unit.volume.drivers.dell_emc import scaleio
|
||||||
|
|
||||||
|
|
||||||
|
VOLUME_ID = "abcdabcd-1234-abcd-1234-abcdabcdabcd"
|
||||||
|
PROVIDER_ID = "0000000000000001"
|
||||||
|
|
||||||
|
MANAGEABLE_SCALEIO_VOLS = [
|
||||||
|
{
|
||||||
|
"volumeType": "ThinProvisioned",
|
||||||
|
"storagePoolId": "6c6dc54500000000",
|
||||||
|
"sizeInKb": 8388608,
|
||||||
|
"name": "volume1",
|
||||||
|
"id": PROVIDER_ID,
|
||||||
|
"mappedSdcInfo": [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"volumeType": "ThinProvisioned",
|
||||||
|
"storagePoolId": "6c6dc54500000000",
|
||||||
|
"sizeInKb": 8388608,
|
||||||
|
"name": "volume2",
|
||||||
|
"id": "0000000000000002",
|
||||||
|
"mappedSdcInfo": [],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"volumeType": "ThickProvisioned",
|
||||||
|
"storagePoolId": "6c6dc54500000000",
|
||||||
|
"sizeInKb": 8388608,
|
||||||
|
"name": "volume3",
|
||||||
|
"id": "0000000000000003",
|
||||||
|
"mappedSdcInfo": [],
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
SCALEIO_SNAPSHOT = {
|
||||||
|
"volumeType": "Snapshot",
|
||||||
|
"storagePoolId": "6c6dc54500000000",
|
||||||
|
"sizeInKb": 8388608,
|
||||||
|
"name": "snapshot1",
|
||||||
|
"id": "1000000000000001",
|
||||||
|
"mappedSdcInfo": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
MANAGEABLE_SCALEIO_VOL_REFS = [
|
||||||
|
{
|
||||||
|
'reference': {'source-id': PROVIDER_ID},
|
||||||
|
'size': 8,
|
||||||
|
'safe_to_manage': True,
|
||||||
|
'reason_not_safe': None,
|
||||||
|
'cinder_id': None,
|
||||||
|
'extra_info': {
|
||||||
|
"volumeType": "ThinProvisioned",
|
||||||
|
"name": "volume1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'reference': {'source-id': '0000000000000002'},
|
||||||
|
'size': 8,
|
||||||
|
'safe_to_manage': True,
|
||||||
|
'reason_not_safe': None,
|
||||||
|
'cinder_id': None,
|
||||||
|
'extra_info': {
|
||||||
|
"volumeType": "ThinProvisioned",
|
||||||
|
"name": "volume2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'reference': {'source-id': '0000000000000003'},
|
||||||
|
'size': 8,
|
||||||
|
'safe_to_manage': True,
|
||||||
|
'reason_not_safe': None,
|
||||||
|
'cinder_id': None,
|
||||||
|
'extra_info': {
|
||||||
|
"volumeType": "ThickProvisioned",
|
||||||
|
"name": "volume3"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ddt.ddt
|
||||||
|
class ScaleIOManageableCase(scaleio.TestScaleIODriver):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
"""Setup a test case environment."""
|
||||||
|
super(ScaleIOManageableCase, self).setUp()
|
||||||
|
|
||||||
|
def _test_get_manageable_things(self,
|
||||||
|
scaleio_objects=MANAGEABLE_SCALEIO_VOLS,
|
||||||
|
expected_refs=MANAGEABLE_SCALEIO_VOL_REFS,
|
||||||
|
cinder_objs=list()):
|
||||||
|
marker = mock.Mock()
|
||||||
|
limit = mock.Mock()
|
||||||
|
offset = mock.Mock()
|
||||||
|
sort_keys = mock.Mock()
|
||||||
|
sort_dirs = mock.Mock()
|
||||||
|
|
||||||
|
self.HTTPS_MOCK_RESPONSES = {
|
||||||
|
self.RESPONSE_MODE.Valid: {
|
||||||
|
'instances/StoragePool::test_pool/relationships/Volume':
|
||||||
|
scaleio_objects
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
with mock.patch('cinder.volume.utils.paginate_entries_list') as mpage:
|
||||||
|
test_func = self.driver.get_manageable_volumes
|
||||||
|
test_func(cinder_objs, marker, limit, offset, sort_keys, sort_dirs)
|
||||||
|
mpage.assert_called_once_with(
|
||||||
|
expected_refs,
|
||||||
|
marker,
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
sort_keys,
|
||||||
|
sort_dirs
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_get_manageable_volumes(self):
|
||||||
|
"""Default success case.
|
||||||
|
|
||||||
|
Given a list of scaleio volumes from the REST API, give back a list
|
||||||
|
of volume references.
|
||||||
|
"""
|
||||||
|
|
||||||
|
self._test_get_manageable_things()
|
||||||
|
|
||||||
|
def test_get_manageable_volumes_connected_vol(self):
|
||||||
|
"""Make sure volumes connected to hosts are flagged as unsafe."""
|
||||||
|
mapped_sdc = deepcopy(MANAGEABLE_SCALEIO_VOLS)
|
||||||
|
mapped_sdc[0]['mappedSdcInfo'] = ["host1"]
|
||||||
|
mapped_sdc[1]['mappedSdcInfo'] = ["host1", "host2"]
|
||||||
|
|
||||||
|
# change up the expected results
|
||||||
|
expected_refs = deepcopy(MANAGEABLE_SCALEIO_VOL_REFS)
|
||||||
|
for x in range(len(mapped_sdc)):
|
||||||
|
sdc = mapped_sdc[x]['mappedSdcInfo']
|
||||||
|
if sdc and len(sdc) > 0:
|
||||||
|
expected_refs[x]['safe_to_manage'] = False
|
||||||
|
expected_refs[x]['reason_not_safe'] \
|
||||||
|
= 'Volume mapped to %d host(s).' % len(sdc)
|
||||||
|
|
||||||
|
self._test_get_manageable_things(expected_refs=expected_refs,
|
||||||
|
scaleio_objects=mapped_sdc)
|
||||||
|
|
||||||
|
def test_get_manageable_volumes_already_managed(self):
|
||||||
|
"""Make sure volumes already owned by cinder are flagged as unsafe."""
|
||||||
|
cinder_vol = fake_volume.fake_volume_obj(mock.MagicMock())
|
||||||
|
cinder_vol.id = VOLUME_ID
|
||||||
|
cinder_vol.provider_id = PROVIDER_ID
|
||||||
|
cinders_vols = [cinder_vol]
|
||||||
|
|
||||||
|
# change up the expected results
|
||||||
|
expected_refs = deepcopy(MANAGEABLE_SCALEIO_VOL_REFS)
|
||||||
|
expected_refs[0]['reference'] = {'source-id': PROVIDER_ID}
|
||||||
|
expected_refs[0]['safe_to_manage'] = False
|
||||||
|
expected_refs[0]['reason_not_safe'] = 'Volume already managed.'
|
||||||
|
expected_refs[0]['cinder_id'] = VOLUME_ID
|
||||||
|
|
||||||
|
self._test_get_manageable_things(expected_refs=expected_refs,
|
||||||
|
cinder_objs=cinders_vols)
|
||||||
|
|
||||||
|
def test_get_manageable_volumes_no_snapshots(self):
|
||||||
|
"""Make sure refs returned do not include snapshots."""
|
||||||
|
volumes = deepcopy(MANAGEABLE_SCALEIO_VOLS)
|
||||||
|
volumes.append(SCALEIO_SNAPSHOT)
|
||||||
|
|
||||||
|
self._test_get_manageable_things(scaleio_objects=volumes)
|
||||||
|
|
||||||
|
def test_get_manageable_volumes_no_scaleio_volumes(self):
|
||||||
|
"""Expect no refs to be found if no volumes are on ScaleIO."""
|
||||||
|
self._test_get_manageable_things(scaleio_objects=[],
|
||||||
|
expected_refs=[])
|
@ -37,6 +37,7 @@ from cinder import exception
|
|||||||
from cinder.i18n import _
|
from cinder.i18n import _
|
||||||
from cinder.image import image_utils
|
from cinder.image import image_utils
|
||||||
from cinder import interface
|
from cinder import interface
|
||||||
|
from cinder import objects
|
||||||
from cinder import utils
|
from cinder import utils
|
||||||
|
|
||||||
from cinder.objects import fields
|
from cinder.objects import fields
|
||||||
@ -326,6 +327,10 @@ class ScaleIODriver(driver.VolumeDriver):
|
|||||||
def _version_greater_than_or_equal(ver1, ver2):
|
def _version_greater_than_or_equal(ver1, ver2):
|
||||||
return version.LooseVersion(ver1) >= version.LooseVersion(ver2)
|
return version.LooseVersion(ver1) >= version.LooseVersion(ver2)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _convert_kb_to_gib(size):
|
||||||
|
return int(math.ceil(float(size) / units.Mi))
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _id_to_base64(id):
|
def _id_to_base64(id):
|
||||||
# Base64 encode the id to get a volume name less than 32 characters due
|
# Base64 encode the id to get a volume name less than 32 characters due
|
||||||
@ -1159,6 +1164,165 @@ class ScaleIODriver(driver.VolumeDriver):
|
|||||||
self._manage_existing_check_legal_response(r, existing_ref)
|
self._manage_existing_check_legal_response(r, existing_ref)
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
def _get_protection_domain_id(self):
|
||||||
|
""""Get the id of the configured protection domain"""
|
||||||
|
|
||||||
|
if self.protection_domain_id:
|
||||||
|
return self.protection_domain_id
|
||||||
|
|
||||||
|
if not self.protection_domain_name:
|
||||||
|
msg = _("Must specify protection domain name or"
|
||||||
|
" protection domain id.")
|
||||||
|
raise exception.VolumeBackendAPIException(data=msg)
|
||||||
|
|
||||||
|
domain_name = self.protection_domain_name
|
||||||
|
encoded_domain_name = urllib.parse.quote(domain_name, '')
|
||||||
|
req_vars = {'server_ip': self.server_ip,
|
||||||
|
'server_port': self.server_port,
|
||||||
|
'encoded_domain_name': encoded_domain_name}
|
||||||
|
request = ("https://%(server_ip)s:%(server_port)s"
|
||||||
|
"/api/types/Domain/instances/getByName::"
|
||||||
|
"%(encoded_domain_name)s") % req_vars
|
||||||
|
LOG.debug("ScaleIO get domain id by name request: %s.", request)
|
||||||
|
|
||||||
|
r, domain_id = self._execute_scaleio_get_request(request)
|
||||||
|
|
||||||
|
if not domain_id:
|
||||||
|
msg = (_("Domain with name %s wasn't found.")
|
||||||
|
% self.protection_domain_name)
|
||||||
|
LOG.error(msg)
|
||||||
|
raise exception.VolumeBackendAPIException(data=msg)
|
||||||
|
if r.status_code != http_client.OK and "errorCode" in domain_id:
|
||||||
|
msg = (_("Error getting domain id from name %(name)s: %(id)s.")
|
||||||
|
% {'name': self.protection_domain_name,
|
||||||
|
'id': domain_id['message']})
|
||||||
|
LOG.error(msg)
|
||||||
|
raise exception.VolumeBackendAPIException(data=msg)
|
||||||
|
|
||||||
|
LOG.info("Domain id is %s.", domain_id)
|
||||||
|
|
||||||
|
return domain_id
|
||||||
|
|
||||||
|
def _get_storage_pool_id(self):
|
||||||
|
"""Get the id of the configured storage pool"""
|
||||||
|
|
||||||
|
if self.storage_pool_id:
|
||||||
|
return self.storage_pool_id
|
||||||
|
|
||||||
|
if not self.protection_domain_name:
|
||||||
|
msg = _("Must specify storage pool name or"
|
||||||
|
" storage pool id.")
|
||||||
|
raise exception.VolumeBackendAPIException(data=msg)
|
||||||
|
|
||||||
|
domain_id = self._get_protection_domain_id()
|
||||||
|
pool_name = self.storage_pool_name
|
||||||
|
encoded_pool_name = urllib.parse.quote(pool_name, '')
|
||||||
|
req_vars = {'server_ip': self.server_ip,
|
||||||
|
'server_port': self.server_port,
|
||||||
|
'domain_id': domain_id,
|
||||||
|
'encoded_pool_name': encoded_pool_name}
|
||||||
|
request = ("https://%(server_ip)s:%(server_port)s"
|
||||||
|
"/api/types/Pool/instances/getByName::"
|
||||||
|
"%(domain_id)s,%(encoded_pool_name)s") % req_vars
|
||||||
|
LOG.debug("ScaleIO get pool id by name request: %s.", request)
|
||||||
|
r, pool_id = self._execute_scaleio_get_request(request)
|
||||||
|
|
||||||
|
if not pool_id:
|
||||||
|
msg = (_("Pool with name %(pool_name)s wasn't found in "
|
||||||
|
"domain %(domain_id)s.")
|
||||||
|
% {'pool_name': pool_name,
|
||||||
|
'domain_id': domain_id})
|
||||||
|
LOG.error(msg)
|
||||||
|
raise exception.VolumeBackendAPIException(data=msg)
|
||||||
|
if r.status_code != http_client.OK and "errorCode" in pool_id:
|
||||||
|
msg = (_("Error getting pool id from name %(pool_name)s: "
|
||||||
|
"%(err_msg)s.")
|
||||||
|
% {'pool_name': pool_name,
|
||||||
|
'err_msg': pool_id['message']})
|
||||||
|
LOG.error(msg)
|
||||||
|
raise exception.VolumeBackendAPIException(data=msg)
|
||||||
|
|
||||||
|
LOG.info("Pool id is %s.", pool_id)
|
||||||
|
|
||||||
|
return pool_id
|
||||||
|
|
||||||
|
def _get_all_scaleio_volumes(self):
|
||||||
|
"""Gets list of all SIO volumes in PD and SP"""
|
||||||
|
|
||||||
|
sp_id = self._get_storage_pool_id()
|
||||||
|
|
||||||
|
req_vars = {'server_ip': self.server_ip,
|
||||||
|
'server_port': self.server_port,
|
||||||
|
'storage_pool_id': sp_id}
|
||||||
|
request = ("https://%(server_ip)s:%(server_port)s"
|
||||||
|
"/api/instances/StoragePool::%(storage_pool_id)s"
|
||||||
|
"/relationships/Volume") % req_vars
|
||||||
|
LOG.info("ScaleIO get volumes in SP: %s.",
|
||||||
|
request)
|
||||||
|
r, volumes = self._execute_scaleio_get_request(request)
|
||||||
|
|
||||||
|
if r.status_code != http_client.OK:
|
||||||
|
msg = (_("Error calling api "
|
||||||
|
"status code: %d") % r.status_code)
|
||||||
|
raise exception.VolumeBackendAPIException(data=msg)
|
||||||
|
|
||||||
|
return volumes
|
||||||
|
|
||||||
|
def get_manageable_volumes(self, cinder_volumes, marker, limit, offset,
|
||||||
|
sort_keys, sort_dirs):
|
||||||
|
"""List volumes on the backend available for management by Cinder.
|
||||||
|
|
||||||
|
Rule out volumes that are mapped to an SDC or
|
||||||
|
are already in the list of cinder_volumes.
|
||||||
|
Return references of the volume ids for any others.
|
||||||
|
"""
|
||||||
|
|
||||||
|
all_sio_volumes = self._get_all_scaleio_volumes()
|
||||||
|
|
||||||
|
# Put together a map of existing cinder volumes on the array
|
||||||
|
# so we can lookup cinder id's to SIO id
|
||||||
|
existing_vols = {}
|
||||||
|
for cinder_vol in cinder_volumes:
|
||||||
|
provider_id = cinder_vol['provider_id']
|
||||||
|
existing_vols[provider_id] = cinder_vol.name_id
|
||||||
|
|
||||||
|
manageable_volumes = []
|
||||||
|
for sio_vol in all_sio_volumes:
|
||||||
|
cinder_id = existing_vols.get(sio_vol['id'])
|
||||||
|
is_safe = True
|
||||||
|
reason = None
|
||||||
|
|
||||||
|
if sio_vol['mappedSdcInfo']:
|
||||||
|
is_safe = False
|
||||||
|
numHosts = len(sio_vol['mappedSdcInfo'])
|
||||||
|
reason = _('Volume mapped to %d host(s).') % numHosts
|
||||||
|
|
||||||
|
if cinder_id:
|
||||||
|
is_safe = False
|
||||||
|
reason = _("Volume already managed.")
|
||||||
|
|
||||||
|
if sio_vol['volumeType'] != 'Snapshot':
|
||||||
|
manageable_volumes.append({
|
||||||
|
'reference': {'source-id': sio_vol['id']},
|
||||||
|
'size': self._convert_kb_to_gib(sio_vol['sizeInKb']),
|
||||||
|
'safe_to_manage': is_safe,
|
||||||
|
'reason_not_safe': reason,
|
||||||
|
'cinder_id': cinder_id,
|
||||||
|
'extra_info': {'volumeType': sio_vol['volumeType'],
|
||||||
|
'name': sio_vol['name']}})
|
||||||
|
|
||||||
|
return volume_utils.paginate_entries_list(
|
||||||
|
manageable_volumes, marker, limit, offset, sort_keys, sort_dirs)
|
||||||
|
|
||||||
|
def _is_managed(self, volume_id):
|
||||||
|
lst = objects.VolumeList.get_all_by_host(context.get_admin_context(),
|
||||||
|
self.host)
|
||||||
|
for vol in lst:
|
||||||
|
if vol.provider_id == volume_id:
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
def manage_existing(self, volume, existing_ref):
|
def manage_existing(self, volume, existing_ref):
|
||||||
"""Manage an existing ScaleIO volume.
|
"""Manage an existing ScaleIO volume.
|
||||||
|
|
||||||
@ -1246,8 +1410,7 @@ class ScaleIODriver(driver.VolumeDriver):
|
|||||||
LOG.info("ScaleIO get volume by id request: %s.", request)
|
LOG.info("ScaleIO get volume by id request: %s.", request)
|
||||||
return request
|
return request
|
||||||
|
|
||||||
@staticmethod
|
def _manage_existing_check_legal_response(self, response, existing_ref):
|
||||||
def _manage_existing_check_legal_response(response, existing_ref):
|
|
||||||
if response.status_code != http_client.OK:
|
if response.status_code != http_client.OK:
|
||||||
reason = (_("Error managing volume: %s.") % response.json()[
|
reason = (_("Error managing volume: %s.") % response.json()[
|
||||||
'message'])
|
'message'])
|
||||||
@ -1256,6 +1419,15 @@ class ScaleIODriver(driver.VolumeDriver):
|
|||||||
reason=reason
|
reason=reason
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# check if it is already managed
|
||||||
|
if self._is_managed(response.json()['id']):
|
||||||
|
reason = _("manage_existing cannot manage a volume "
|
||||||
|
"that is already being managed.")
|
||||||
|
raise exception.ManageExistingInvalidReference(
|
||||||
|
existing_ref=existing_ref,
|
||||||
|
reason=reason
|
||||||
|
)
|
||||||
|
|
||||||
if response.json()['mappedSdcInfo'] is not None:
|
if response.json()['mappedSdcInfo'] is not None:
|
||||||
reason = _("manage_existing cannot manage a volume "
|
reason = _("manage_existing cannot manage a volume "
|
||||||
"connected to hosts. Please disconnect this volume "
|
"connected to hosts. Please disconnect this volume "
|
||||||
|
@ -0,0 +1,3 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- Added ability to list all manageable volumes within ScaleIO Driver.
|
Loading…
Reference in New Issue
Block a user