ScaleIO: Fixing support for SIO 1.3x

A few changes necessary to make driver compatible with
ScaleIO version 1.3x
- refactored SIO REST calls to go through common code
- obtain REST API version number dynamically
- allow configuration to override what API version to use
- Make API calls based upon API version
- Add unit tests to validate calls to retrieve version

This commit adds a new configuration option, called:
   sio_server_api_version
   - This can be used to override the value
     retrieved from the ScaleIO server at runtime
   - This is helpful for manual testing, but should
     not be needed in a production environment

DocImpact

Change-Id: I741bc5384fa3a5afcb80f52f4b39b4123e2b8e11
Closes-Bug: #1644904
This commit is contained in:
Eric Young 2017-03-22 12:36:50 -04:00
parent a01ec6ecaa
commit 09bb6c6072
3 changed files with 182 additions and 99 deletions

View File

@ -46,6 +46,8 @@ class ScaleIODriver(driver.ScaleIODriver):
override='test_domain:test_pool') override='test_domain:test_pool')
configuration.set_override('max_over_subscription_ratio', configuration.set_override('max_over_subscription_ratio',
override=5.0) override=5.0)
configuration.set_override('sio_server_api_version',
override='2.0.0')
if 'san_thin_provision' in kwargs: if 'san_thin_provision' in kwargs:
configuration.set_override( configuration.set_override(
'san_thin_provision', 'san_thin_provision',

View File

@ -0,0 +1,93 @@
# 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.
import ddt
from cinder import exception
from cinder.tests.unit.volume.drivers.dell_emc import scaleio
@ddt.ddt
class TestMultipleVersions(scaleio.TestScaleIODriver):
version = '1.2.3.4'
good_versions = ['1.2.3.4',
'101.102.103.104.105.106.107',
'1.0'
]
bad_versions = ['bad',
'bad.version.number',
'1.0b',
'.6'
]
# Test cases for ``ScaleIODriver._get_server_api_version()``
def setUp(self):
"""Setup a test case environment."""
super(TestMultipleVersions, self).setUp()
self.HTTPS_MOCK_RESPONSES = {
self.RESPONSE_MODE.Valid: {
'version': '"{}"'.format(self.version),
},
self.RESPONSE_MODE.Invalid: {
'version': None,
},
self.RESPONSE_MODE.BadStatus: {
'version': self.BAD_STATUS_RESPONSE,
},
}
def test_version_api_fails(self):
"""version api returns a non-200 response."""
self.set_https_response_mode(self.RESPONSE_MODE.Invalid)
self.assertRaises(exception.VolumeBackendAPIException,
self.test_version)
def test_version(self):
"""Valid version request."""
self.driver._get_server_api_version(False)
def test_version_badstatus_response(self):
"""Version api returns a bad response."""
self.set_https_response_mode(self.RESPONSE_MODE.BadStatus)
self.assertRaises(exception.VolumeBackendAPIException,
self.test_version)
def setup_response(self):
self.HTTPS_MOCK_RESPONSES = {
self.RESPONSE_MODE.Valid: {
'version': '"{}"'.format(self.version),
},
}
def test_version_badversions(self):
"""Version api returns an invalid version number."""
for vers in self.bad_versions:
self.version = vers
self.setup_response()
self.assertRaises(exception.VolumeBackendAPIException,
self.test_version)
def test_version_goodversions(self):
"""Version api returns a valid version number."""
for vers in self.good_versions:
self.version = vers
self.setup_response()
self.driver._get_server_api_version(False)
self.assertEqual(
self.driver._get_server_api_version(False),
vers
)

View File

@ -18,13 +18,14 @@ Driver for Dell EMC ScaleIO based on ScaleIO remote CLI.
import base64 import base64
import binascii import binascii
from distutils import version
import json import json
import math import math
from os_brick.initiator import connector from os_brick.initiator import connector
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
import re
import requests import requests
import six import six
from six.moves import urllib from six.moves import urllib
@ -69,6 +70,8 @@ scaleio_opts = [
help='Storage Pool name.'), help='Storage Pool name.'),
cfg.StrOpt('sio_storage_pool_id', cfg.StrOpt('sio_storage_pool_id',
help='Storage Pool ID.'), help='Storage Pool ID.'),
cfg.StrOpt('sio_server_api_version',
help='ScaleIO API version.'),
cfg.FloatOpt('sio_max_over_subscription_ratio', cfg.FloatOpt('sio_max_over_subscription_ratio',
# This option exists to provide a default value for the # This option exists to provide a default value for the
# ScaleIO driver which is different than the global default. # ScaleIO driver which is different than the global default.
@ -111,7 +114,9 @@ SIO_MAX_OVERSUBSCRIPTION_RATIO = 10.0
class ScaleIODriver(driver.VolumeDriver): class ScaleIODriver(driver.VolumeDriver):
"""Dell EMC ScaleIO Driver.""" """Dell EMC ScaleIO Driver."""
VERSION = "2.0" VERSION = "2.0.1"
# Major changes
# 2.0.1: Added support for SIO 1.3x in addition to 2.0.x
# ThirdPartySystems wiki # ThirdPartySystems wiki
CI_WIKI_NAME = "EMC_ScaleIO_CI" CI_WIKI_NAME = "EMC_ScaleIO_CI"
@ -129,6 +134,7 @@ class ScaleIODriver(driver.VolumeDriver):
self.server_username = self.configuration.san_login self.server_username = self.configuration.san_login
self.server_password = self.configuration.san_password self.server_password = self.configuration.san_password
self.server_token = None self.server_token = None
self.server_api_version = self.configuration.sio_server_api_version
self.verify_server_certificate = ( self.verify_server_certificate = (
self.configuration.sio_verify_server_certificate) self.configuration.sio_verify_server_certificate)
self.server_certificate_path = None self.server_certificate_path = None
@ -299,6 +305,14 @@ class ScaleIODriver(driver.VolumeDriver):
"of OpenStack. Please use QoS specs.") "of OpenStack. Please use QoS specs.")
return qos_limit if qos_limit is not None else extraspecs_limit return qos_limit if qos_limit is not None else extraspecs_limit
@staticmethod
def _version_greater_than(ver1, ver2):
return version.LooseVersion(ver1) > version.LooseVersion(ver2)
@staticmethod
def _version_greater_than_or_equal(ver1, ver2):
return version.LooseVersion(ver1) >= version.LooseVersion(ver2)
@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
@ -345,8 +359,6 @@ class ScaleIODriver(driver.VolumeDriver):
'domain_id': protection_domain_id, 'domain_id': protection_domain_id,
'domain_name': protection_domain_name}) 'domain_name': protection_domain_name})
verify_cert = self._get_verify_cert()
if storage_pool_name: if storage_pool_name:
self.storage_pool_name = storage_pool_name self.storage_pool_name = storage_pool_name
self.storage_pool_id = None self.storage_pool_id = None
@ -377,15 +389,9 @@ class ScaleIODriver(driver.VolumeDriver):
"%(encoded_domain_name)s") % req_vars "%(encoded_domain_name)s") % req_vars
LOG.info("ScaleIO get domain id by name request: %s.", LOG.info("ScaleIO get domain id by name request: %s.",
request) request)
r = requests.get(
request,
auth=(
self.server_username,
self.server_token),
verify=verify_cert)
r = self._check_response(r, request)
domain_id = r.json() r, domain_id = self._execute_scaleio_get_request(request)
if not domain_id: if not domain_id:
msg = (_("Domain with name %s wasn't found.") msg = (_("Domain with name %s wasn't found.")
% self.protection_domain_name) % self.protection_domain_name)
@ -411,13 +417,8 @@ class ScaleIODriver(driver.VolumeDriver):
"/api/types/Pool/instances/getByName::" "/api/types/Pool/instances/getByName::"
"%(domain_id)s,%(encoded_domain_name)s") % req_vars "%(domain_id)s,%(encoded_domain_name)s") % req_vars
LOG.info("ScaleIO get pool id by name request: %s.", request) LOG.info("ScaleIO get pool id by name request: %s.", request)
r = requests.get( r, pool_id = self._execute_scaleio_get_request(request)
request,
auth=(
self.server_username,
self.server_token),
verify=verify_cert)
pool_id = r.json()
if not pool_id: if not pool_id:
msg = (_("Pool with name %(pool_name)s wasn't found in " msg = (_("Pool with name %(pool_name)s wasn't found in "
"domain %(domain_id)s.") "domain %(domain_id)s.")
@ -449,19 +450,12 @@ class ScaleIODriver(driver.VolumeDriver):
'storagePoolId': pool_id} 'storagePoolId': pool_id}
LOG.info("Params for add volume request: %s.", params) LOG.info("Params for add volume request: %s.", params)
r = requests.post( req_vars = {'server_ip': self.server_ip,
"https://" + 'server_port': self.server_port}
self.server_ip + request = ("https://%(server_ip)s:%(server_port)s"
":" + "/api/types/Volume/instances") % req_vars
self.server_port + r, response = self._execute_scaleio_post_request(params, request)
"/api/types/Volume/instances",
data=json.dumps(params),
headers=self._get_headers(),
auth=(
self.server_username,
self.server_token),
verify=verify_cert)
response = r.json()
LOG.info("Add volume response: %s", response) LOG.info("Add volume response: %s", response)
if r.status_code != OK_STATUS_CODE and "errorCode" in response: if r.status_code != OK_STATUS_CODE and "errorCode" in response:
@ -524,7 +518,11 @@ class ScaleIODriver(driver.VolumeDriver):
self.server_token), self.server_token),
verify=self._get_verify_cert()) verify=self._get_verify_cert())
r = self._check_response(r, request, False, params) r = self._check_response(r, request, False, params)
response = r.json() response = None
try:
response = r.json()
except ValueError:
response = None
return r, response return r, response
def _check_response(self, response, request, is_get_request=True, def _check_response(self, response, request, is_get_request=True,
@ -562,6 +560,31 @@ class ScaleIODriver(driver.VolumeDriver):
return res return res
return response return response
def _get_server_api_version(self, fromcache=True):
if self.server_api_version is None or fromcache is False:
request = (
"https://" + self.server_ip +
":" + self.server_port + "/api/version")
r, unused = self._execute_scaleio_get_request(request)
if r.status_code == OK_STATUS_CODE:
self.server_api_version = r.text.replace('\"', '')
LOG.info("REST API Version: %(api_version)s",
{'api_version': self.server_api_version})
else:
msg = (_("Error calling version api "
"status code: %d") % r.status_code)
raise exception.VolumeBackendAPIException(data=msg)
# make sure the response was valid
pattern = re.compile("^\d+(\.\d+)*$")
if not pattern.match(self.server_api_version):
msg = (_("Error calling version api "
"response: %s") % r.text)
raise exception.VolumeBackendAPIException(data=msg)
return self.server_api_version
def create_volume_from_snapshot(self, volume, snapshot): def create_volume_from_snapshot(self, volume, snapshot):
"""Creates a volume from a snapshot.""" """Creates a volume from a snapshot."""
# We interchange 'volume' and 'snapshot' because in ScaleIO # We interchange 'volume' and 'snapshot' because in ScaleIO
@ -666,8 +689,6 @@ class ScaleIODriver(driver.VolumeDriver):
self._delete_volume(volume_id) self._delete_volume(volume_id)
def _delete_volume(self, vol_id): def _delete_volume(self, vol_id):
verify_cert = self._get_verify_cert()
req_vars = {'server_ip': self.server_ip, req_vars = {'server_ip': self.server_ip,
'server_port': self.server_port, 'server_port': self.server_port,
'vol_id': six.text_type(vol_id)} 'vol_id': six.text_type(vol_id)}
@ -684,34 +705,16 @@ class ScaleIODriver(driver.VolumeDriver):
LOG.info("Trying to unmap volume from all sdcs" LOG.info("Trying to unmap volume from all sdcs"
" before deletion: %s.", " before deletion: %s.",
request) request)
r = requests.post( r, unused = self._execute_scaleio_post_request(params, request)
request,
data=json.dumps(params),
headers=self._get_headers(),
auth=(
self.server_username,
self.server_token),
verify=verify_cert
)
r = self._check_response(r, request, False, params)
LOG.debug("Unmap volume response: %s.", r.text) LOG.debug("Unmap volume response: %s.", r.text)
params = {'removeMode': 'ONLY_ME'} params = {'removeMode': 'ONLY_ME'}
request = ("https://%(server_ip)s:%(server_port)s" request = ("https://%(server_ip)s:%(server_port)s"
"/api/instances/Volume::%(vol_id)s" "/api/instances/Volume::%(vol_id)s"
"/action/removeVolume") % req_vars "/action/removeVolume") % req_vars
r = requests.post( r, response = self._execute_scaleio_post_request(params, request)
request,
data=json.dumps(params),
headers=self._get_headers(),
auth=(self.server_username,
self.server_token),
verify=verify_cert
)
r = self._check_response(r, request, False, params)
if r.status_code != OK_STATUS_CODE: if r.status_code != OK_STATUS_CODE:
response = r.json()
error_code = response['errorCode'] error_code = response['errorCode']
if error_code == VOLUME_NOT_FOUND_ERROR: if error_code == VOLUME_NOT_FOUND_ERROR:
LOG.warning("Ignoring error in delete volume %s:" LOG.warning("Ignoring error in delete volume %s:"
@ -854,15 +857,7 @@ class ScaleIODriver(driver.VolumeDriver):
LOG.info("username: %(username)s, verify_cert: %(verify)s.", LOG.info("username: %(username)s, verify_cert: %(verify)s.",
{'username': self.server_username, {'username': self.server_username,
'verify': verify_cert}) 'verify': verify_cert})
r = requests.get( r, domain_id = self._execute_scaleio_get_request(request)
request,
auth=(
self.server_username,
self.server_token),
verify=verify_cert)
r = self._check_response(r, request)
LOG.info("Get domain by name response: %s", r.text)
domain_id = r.json()
if not domain_id: if not domain_id:
msg = (_("Domain with name %s wasn't found.") msg = (_("Domain with name %s wasn't found.")
% self.protection_domain_name) % self.protection_domain_name)
@ -887,13 +882,7 @@ class ScaleIODriver(driver.VolumeDriver):
"/api/types/Pool/instances/getByName::" "/api/types/Pool/instances/getByName::"
"%(domain_id)s,%(encoded_pool_name)s") % req_vars "%(domain_id)s,%(encoded_pool_name)s") % req_vars
LOG.info("ScaleIO get pool id by name request: %s.", request) LOG.info("ScaleIO get pool id by name request: %s.", request)
r = requests.get( r, pool_id = self._execute_scaleio_get_request(request)
request,
auth=(
self.server_username,
self.server_token),
verify=verify_cert)
pool_id = r.json()
if not pool_id: if not pool_id:
msg = (_("Pool with name %(pool)s wasn't found in domain " msg = (_("Pool with name %(pool)s wasn't found in domain "
"%(domain)s.") "%(domain)s.")
@ -914,20 +903,22 @@ class ScaleIODriver(driver.VolumeDriver):
request = ("https://%(server_ip)s:%(server_port)s" request = ("https://%(server_ip)s:%(server_port)s"
"/api/types/StoragePool/instances/action/" "/api/types/StoragePool/instances/action/"
"querySelectedStatistics") % req_vars "querySelectedStatistics") % req_vars
# The 'Km' in thinCapacityAllocatedInKm is a bug in REST API # SIO version 2+ added a property...
params = {'ids': [pool_id], 'properties': [ if self._version_greater_than_or_equal(
"capacityAvailableForVolumeAllocationInKb", self._get_server_api_version(),
"capacityLimitInKb", "spareCapacityInKb", "2.0.0"):
"thickCapacityInUseInKb", "thinCapacityAllocatedInKm"]} # The 'Km' in thinCapacityAllocatedInKm is a bug in REST API
r = requests.post( params = {'ids': [pool_id], 'properties': [
request, "capacityAvailableForVolumeAllocationInKb",
data=json.dumps(params), "capacityLimitInKb", "spareCapacityInKb",
headers=self._get_headers(), "thickCapacityInUseInKb", "thinCapacityAllocatedInKm"]}
auth=( else:
self.server_username, params = {'ids': [pool_id], 'properties': [
self.server_token), "capacityAvailableForVolumeAllocationInKb",
verify=verify_cert) "capacityLimitInKb", "spareCapacityInKb",
response = r.json() "thickCapacityInUseInKb"]}
r, response = self._execute_scaleio_post_request(params, request)
LOG.info("Query capacity stats response: %s.", response) LOG.info("Query capacity stats response: %s.", response)
for res in response.values(): for res in response.values():
# Divide by two because ScaleIO creates a copy for each volume # Divide by two because ScaleIO creates a copy for each volume
@ -940,10 +931,16 @@ class ScaleIODriver(driver.VolumeDriver):
free_capacity_gb = ( free_capacity_gb = (
res['capacityAvailableForVolumeAllocationInKb'] / units.Mi) res['capacityAvailableForVolumeAllocationInKb'] / units.Mi)
# Divide by two because ScaleIO creates a copy for each volume # Divide by two because ScaleIO creates a copy for each volume
provisioned_capacity = ( if self._version_greater_than_or_equal(
((res['thickCapacityInUseInKb'] + self._get_server_api_version(),
res['thinCapacityAllocatedInKm']) / 2) / units.Mi) "2.0.0"):
LOG.info("Free capacity of pool %(pool)s is: %(free)s, " provisioned_capacity = (
((res['thickCapacityInUseInKb'] +
res['thinCapacityAllocatedInKm']) / 2) / units.Mi)
else:
provisioned_capacity = (
(res['thickCapacityInUseInKb'] / 2) / units.Mi)
LOG.info("free capacity of pool %(pool)s is: %(free)s, "
"total capacity: %(total)s, " "total capacity: %(total)s, "
"provisioned capacity: %(prov)s", "provisioned capacity: %(prov)s",
{'pool': pool_name, {'pool': pool_name,
@ -1120,18 +1117,9 @@ class ScaleIODriver(driver.VolumeDriver):
LOG.info("ScaleIO rename volume request: %s.", request) LOG.info("ScaleIO rename volume request: %s.", request)
params = {'newName': new_name} params = {'newName': new_name}
r = requests.post( r, response = self._execute_scaleio_post_request(params, request)
request,
data=json.dumps(params),
headers=self._get_headers(),
auth=(self.server_username,
self.server_token),
verify=self._get_verify_cert()
)
r = self._check_response(r, request, False, params)
if r.status_code != OK_STATUS_CODE: if r.status_code != OK_STATUS_CODE:
response = r.json()
error_code = response['errorCode'] error_code = response['errorCode']
if ((error_code == VOLUME_NOT_FOUND_ERROR or if ((error_code == VOLUME_NOT_FOUND_ERROR or
error_code == OLD_VOLUME_NOT_FOUND_ERROR or error_code == OLD_VOLUME_NOT_FOUND_ERROR or