1.0.2 - added FC support, improved error handling
1.0.3 - update logging level, add translation
1.0.4 - support for FC zones
+1.0.5 - add support for XtremIO 4.0
"""
import base64
import json
+import math
import random
import string
import urllib
import urllib2
+from oslo_config import cfg
+from oslo_utils import units
import six
from cinder import exception
LOG = logging.getLogger(__name__)
-
-class XtremIOVolumeDriver(san.SanDriver):
- """Executes commands relating to Volumes."""
-
- VERSION = '1.0.4'
- driver_name = 'XtremIO'
- MIN_XMS_VERSION = [2, 4, 0]
- random = random.Random()
-
- def __init__(self, *args, **kwargs):
- super(XtremIOVolumeDriver, self).__init__(*args, **kwargs)
- self.base64_auth = (base64.encodestring('%s:%s' %
- (self.configuration.san_login,
- self.configuration.san_password))
+CONF = cfg.CONF
+DEFAULT_PROVISIONING_FACTOR = 20.0
+XTREMIO_OPTS = [
+ cfg.StrOpt('xtremio_cluster_name',
+ default='',
+ help='XMS cluster id in multi-cluster environment')]
+
+CONF.register_opts(XTREMIO_OPTS)
+
+RANDOM = random.Random()
+OBJ_NOT_FOUND_ERR = 'obj_not_found'
+VOL_NOT_UNIQUE_ERR = 'vol_obj_name_not_unique'
+VOL_OBJ_NOT_FOUND_ERR = 'vol_obj_not_found'
+ALREADY_MAPPED_ERR = 'already_mapped'
+
+
+class XtremIOClient(object):
+ def __init__(self, configuration, cluster_id):
+ self.configuration = configuration
+ self.cluster_id = cluster_id
+ self.base64_auth = (base64
+ .encodestring('%s:%s' %
+ (self.configuration.san_login,
+ self.configuration.san_password))
.replace('\n', ''))
self.base_url = ('https://%s/api/json/types' %
self.configuration.san_ip)
- self.protocol = None
- self.backend_name = (self.configuration.safe_get(
- 'volume_backend_name') or
- self.driver_name)
- def _create_request(self, request_typ, data, url, urllib):
- if data and request_typ == 'GET':
- url + '?' + urllib.urlencode(data)
+ def _create_request(self, request_typ, data, url, url_data):
+ if request_typ in ('GET', 'DELETE'):
+ data.update(url_data)
+ self.update_url(data, self.cluster_id)
+ url = '%(url)s?%(query)s' % {'query': urllib.urlencode(data,
+ doseq=True),
+ 'url': url}
request = urllib2.Request(url)
- elif data:
- LOG.debug('data: %s', json.dumps(data))
- request = urllib2.Request(url, json.dumps(data))
else:
- request = urllib2.Request(url)
- LOG.debug('quering url: %s', url)
- request.get_method = lambda: request_typ
+ if url_data:
+ url = ('%(url)s?%(query)s' %
+ {'query': urllib.urlencode(url_data, doseq=True),
+ 'url': url})
+
+ self.update_data(data, self.cluster_id)
+ LOG.debug('data: %s', data)
+ request = urllib2.Request(url, json.dumps(data))
+ LOG.debug('%(type)s %(url)s', {'type': request_typ, 'url': url})
+
+ def get_request_type():
+ return request_typ
+ request.get_method = get_request_type
request.add_header("Authorization", "Basic %s" % (self.base64_auth, ))
return request
except (urllib2.HTTPError, ) as exc:
if exc.code == 400 and hasattr(exc, 'read'):
error = json.load(exc)
- if error['message'].endswith('obj_not_found'):
+ err_msg = error['message']
+ if err_msg.endswith(OBJ_NOT_FOUND_ERR):
LOG.warning(_LW("object %(key)s of "
"type %(typ)s not found"),
{'key': key, 'typ': object_type})
raise exception.NotFound()
- elif error['message'] == 'vol_obj_name_not_unique':
+ elif err_msg == VOL_NOT_UNIQUE_ERR:
LOG.error(_LE("can't create 2 volumes with the same name"))
msg = (_('Volume by this name already exists'))
raise exception.VolumeBackendAPIException(data=msg)
+ elif err_msg == VOL_OBJ_NOT_FOUND_ERR:
+ LOG.error(_LE("Can't find volume to map %s"), key)
+ raise exception.VolumeNotFound(volume_id=key)
+ elif ALREADY_MAPPED_ERR in err_msg:
+ raise exception.XtremIOAlreadyMappedError()
LOG.error(_LE('Bad response from XMS, %s'), exc.read())
msg = (_('Exception: %s') % six.text_type(exc))
raise exception.VolumeDriverException(message=msg)
def req(self, object_type='volumes', request_typ='GET', data=None,
name=None, idx=None):
+ if not data:
+ data = {}
if name and idx:
msg = _("can't handle both name and index in req")
LOG.error(msg)
raise exception.VolumeDriverException(message=msg)
url = '%s/%s' % (self.base_url, object_type)
+ url_data = {}
key = None
if name:
- url = '%s?%s' % (url, urllib.urlencode({'name': name}))
+ url_data['name'] = name
key = name
elif idx:
url = '%s/%d' % (url, idx)
key = str(idx)
- request = self._create_request(request_typ, data, url, urllib)
+ request = self._create_request(request_typ, data, url, url_data)
response = self._send_request(object_type, key, request)
str_result = response.read()
if str_result:
'req': request_typ,
'res': str_result})
+ def update_url(self, data, cluster_id):
+ return
+
+ def update_data(self, data, cluster_id):
+ return
+
+ def get_cluster(self):
+ return self.req('clusters', idx=1)['content']
+
+
+class XtremIOClient3(XtremIOClient):
+ def find_lunmap(self, ig_name, vol_name):
+ try:
+ for lm_link in self.req('lun-maps')['lun-maps']:
+ idx = lm_link['href'].split('/')[-1]
+ lm = self.req('lun-maps', idx=int(idx))['content']
+ if lm['ig-name'] == ig_name and lm['vol-name'] == vol_name:
+ return lm
+ except exception.NotFound:
+ raise (exception.VolumeDriverException
+ (_("can't find lunmap, ig:%(ig)s vol:%(vol)s") %
+ {'ig': ig_name, 'vol': vol_name}))
+
+ def num_of_mapped_volumes(self, initiator):
+ cnt = 0
+ for lm_link in self.req('lun-maps')['lun-maps']:
+ idx = lm_link['href'].split('/')[-1]
+ lm = self.req('lun-maps', idx=int(idx))['content']
+ if lm['ig-name'] == initiator:
+ cnt += 1
+ return cnt
+
+ def get_iscsi_portal(self):
+ iscsi_portals = [t['name'] for t in self.req('iscsi-portals')
+ ['iscsi-portals']]
+ # Get a random portal
+ portal_name = RANDOM.choice(iscsi_portals)
+ try:
+ portal = self.req('iscsi-portals',
+ name=portal_name)['content']
+ except exception.NotFound:
+ raise (exception.VolumeBackendAPIException
+ (data=_("iscsi portal, %s, not found") % portal_name))
+
+ return portal
+
+
+class XtremIOClient4(XtremIOClient):
+ def find_lunmap(self, ig_name, vol_name):
+ try:
+ return (self.req('lun-maps',
+ data={'full': 1,
+ 'filter': ['vol-name:eq:%s' % vol_name,
+ 'ig-name:eq:%s' % ig_name]})
+ ['lun-maps'][0])
+ except (KeyError, IndexError):
+ raise exception.VolumeNotFound(volume_id=vol_name)
+
+ def num_of_mapped_volumes(self, initiator):
+ return len(self.req('lun-maps',
+ data={'filter': 'ig-name:eq:%s' % initiator})
+ ['lun-maps'])
+
+ def update_url(self, data, cluster_id):
+ if cluster_id:
+ data['cluster-name'] = cluster_id
+
+ def update_data(self, data, cluster_id):
+ if cluster_id:
+ data['cluster-id'] = cluster_id
+
+ def get_iscsi_portal(self):
+ iscsi_portals = self.req('iscsi-portals',
+ data={'full': 1})['iscsi-portals']
+ return RANDOM.choice(iscsi_portals)
+
+ def get_cluster(self):
+ if self.cluster_id:
+ return self.req('clusters', name=self.cluster_id)['content']
+ else:
+ name = self.req('clusters')['clusters'][0]['name']
+ return self.req('clusters', name=name)['content']
+
+
+class XtremIOVolumeDriver(san.SanDriver):
+ """Executes commands relating to Volumes."""
+
+ VERSION = '1.0.5'
+ driver_name = 'XtremIO'
+ MIN_XMS_VERSION = [3, 0, 0]
+
+ def __init__(self, *args, **kwargs):
+ super(XtremIOVolumeDriver, self).__init__(*args, **kwargs)
+ self.configuration.append_config_values(XTREMIO_OPTS)
+ self.protocol = None
+ self.backend_name = (self.configuration.safe_get('volume_backend_name')
+ or self.driver_name)
+ self.cluster_id = (self.configuration.safe_get('xtremio_cluster_name')
+ or '')
+ self.provisioning_factor = (self.configuration.
+ safe_get('max_over_subscription_ratio')
+ or DEFAULT_PROVISIONING_FACTOR)
+ self._stats = {}
+ self.client = XtremIOClient3(self.configuration, self.cluster_id)
+
def _obj_from_result(self, res):
typ, idx = res['links'][0]['href'].split('/')[-2:]
- return self.req(typ, idx=int(idx))['content']
+ return self.client.req(typ, idx=int(idx))['content']
def check_for_setup_error(self):
try:
- sys = self.req('clusters', idx=1)['content']
+ try:
+ xms = self.client.req('xms', idx=1)['content']
+ version_text = xms['version']
+ except exception.VolumeDriverException:
+ cluster = self.client.req('clusters', idx=1)['content']
+ version_text = cluster['sys-sw-version']
except exception.NotFound:
msg = _("XtremIO not initialized correctly, no clusters found")
raise (exception.VolumeBackendAPIException
(data=msg))
- ver = [int(n) for n in sys['sys-sw-version'].split('-')[0].split('.')]
+ ver = [int(n) for n in version_text.split('-')[0].split('.')]
if ver < self.MIN_XMS_VERSION:
- msg = _('Invalid XtremIO version %s,'
- ' version 2.4 or up is required') % sys['sys-sw-version']
+ msg = (_('Invalid XtremIO version %(cur)s,'
+ ' version %(min)s or up is required') %
+ {'min': self.MIN_XMS_VERSION,
+ 'cur': ver})
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
else:
- LOG.info(_LI('XtremIO SW version %s'), sys['sys-sw-version'])
+ LOG.info(_LI('XtremIO SW version %s'), version_text)
+ if ver[0] >= 4:
+ self.client = XtremIOClient4(self.configuration, self.cluster_id)
def create_volume(self, volume):
"Creates a volume"
'vol-size': str(volume['size']) + 'g'
}
- self.req('volumes', 'POST', data)
+ self.client.req('volumes', 'POST', data)
def create_volume_from_snapshot(self, volume, snapshot):
"""Creates a volume from a snapshot."""
data = {'snap-vol-name': volume['id'],
'ancestor-vol-id': snapshot.id}
- self.req('snapshots', 'POST', data)
+ self.client.req('snapshots', 'POST', data)
def create_cloned_volume(self, volume, src_vref):
"""Creates a clone of the specified volume."""
data = {'snap-vol-name': volume['id'],
'ancestor-vol-id': src_vref['id']}
- self.req('snapshots', 'POST', data)
+ self.client.req('snapshots', 'POST', data)
def delete_volume(self, volume):
"""Deletes a volume."""
try:
- self.req('volumes', 'DELETE', name=volume['id'])
+ self.client.req('volumes', 'DELETE', name=volume['id'])
except exception.NotFound:
LOG.info(_LI("volume %s doesn't exist"), volume['id'])
data = {'snap-vol-name': snapshot.id,
'ancestor-vol-id': snapshot.volume_id}
- self.req('snapshots', 'POST', data)
+ self.client.req('snapshots', 'POST', data)
def delete_snapshot(self, snapshot):
"""Deletes a snapshot."""
try:
- self.req('volumes', 'DELETE', name=snapshot.id)
+ self.client.req('volumes', 'DELETE', name=snapshot.id)
except exception.NotFound:
LOG.info(_LI("snapshot %s doesn't exist"), snapshot.id)
def _update_volume_stats(self):
+ sys = self.client.get_cluster()
+ physical_space = int(sys["ud-ssd-space"]) / units.Mi
+ used_physical_space = int(sys["ud-ssd-space-in-use"]) / units.Mi
+ free_physical = physical_space - used_physical_space
+ actual_prov = int(sys["vol-size"]) / units.Mi
self._stats = {'volume_backend_name': self.backend_name,
'vendor_name': 'EMC',
'driver_version': self.VERSION,
'storage_protocol': self.protocol,
- 'total_capacity_gb': 'infinite',
- 'free_capacity_gb': 'infinite',
- 'reserved_percentage': 0,
+ 'total_capacity_gb': physical_space,
+ 'free_capacity_gb': (free_physical *
+ self.provisioning_factor),
+ 'provisioned_capacity_gb': actual_prov,
+ 'max_over_subscription_ratio': self.provisioning_factor,
+ 'thin_provisioning_support': True,
+ 'thick_provisioning_support': False,
+ 'reserved_percentage':
+ self.configuration.reserved_percentage,
'QoS_support': False}
def get_volume_stats(self, refresh=False):
self._update_volume_stats()
return self._stats
+ def manage_existing(self, volume, existing_ref):
+ """Manages an existing LV."""
+ lv_name = existing_ref['source-name']
+ # Attempt to locate the volume.
+ try:
+ vol_obj = self.client.req('volumes', name=lv_name)['content']
+ except exception.NotFound:
+ kwargs = {'existing_ref': lv_name,
+ 'reason': 'Specified logical volume does not exist.'}
+ raise exception.ManageExistingInvalidReference(**kwargs)
+
+ # Attempt to rename the LV to match the OpenStack internal name.
+ self.client.req('volumes', 'PUT', data={'vol-name': volume['id']},
+ idx=vol_obj['index'])
+
+ def manage_existing_get_size(self, volume, existing_ref):
+ """Return size of an existing LV for manage_existing."""
+ # Check that the reference is valid
+ if 'source-name' not in existing_ref:
+ reason = _('Reference must contain source-name element.')
+ raise exception.ManageExistingInvalidReference(
+ existing_ref=existing_ref, reason=reason)
+ lv_name = existing_ref['source-name']
+ # Attempt to locate the volume.
+ try:
+ vol_obj = self.client.req('volumes', name=lv_name)['content']
+ except exception.NotFound:
+ kwargs = {'existing_ref': lv_name,
+ 'reason': 'Specified logical volume does not exist.'}
+ raise exception.ManageExistingInvalidReference(**kwargs)
+ # LV size is returned in gigabytes. Attempt to parse size as a float
+ # and round up to the next integer.
+ lv_size = int(math.ceil(int(vol_obj['vol-size']) / units.Mi))
+
+ return lv_size
+
+ def unmanage(self, volume):
+ """Removes the specified volume from Cinder management."""
+ # trying to rename the volume to [cinder name]-unmanged
+ try:
+ self.client.req('volumes', 'PUT', name=volume['id'],
+ data={'vol-name': volume['name'] + '-unmanged'})
+ except exception.NotFound:
+ LOG.info(_LI("Volume with the name %s wasn't found,"
+ " can't unmanage"),
+ volume['id'])
+ raise exception.VolumeNotFound(volume_id=volume['id'])
+
def extend_volume(self, volume, new_size):
"""Extend an existing volume's size."""
- data = {'vol-size': str(new_size) + 'g'}
+ data = {'vol-size': six.text_type(new_size) + 'g'}
try:
- self.req('volumes', 'PUT', data, name=volume['id'])
+ self.client.req('volumes', 'PUT', data, name=volume['id'])
except exception.NotFound:
msg = _("can't find the volume to extend")
raise (exception.VolumeDriverException(message=msg))
def terminate_connection(self, volume, connector, **kwargs):
"""Disallow connection from connector"""
try:
- ig = self.req('initiator-groups',
- name=self._get_ig(connector))['content']
- tg = self.req('target-groups', name='Default')['content']
- vol = self.req('volumes', name=volume['id'])['content']
-
- lm_name = '%s_%s_%s' % (str(vol['index']),
- str(ig['index']) if ig else 'any',
- str(tg['index']))
- LOG.info(_LI('removing lun map %s'), lm_name)
- self.req('lun-maps', 'DELETE', name=lm_name)
+ ig = self.client.req('initiator-groups',
+ name=self._get_ig(connector))['content']
+ tg = self.client.req('target-groups', name='Default')['content']
+ vol = self.client.req('volumes', name=volume['id'])['content']
+
+ lm_name = '%s_%s_%s' % (six.text_type(vol['index']),
+ six.text_type(ig['index'])
+ if ig else 'any',
+ six.text_type(tg['index']))
+ LOG.debug('removing lun map %s', lm_name)
+ self.client.req('lun-maps', 'DELETE', name=lm_name)
except exception.NotFound:
LOG.warning(_LW("terminate_connection: lun map not found"))
- def _find_lunmap(self, ig_name, vol_name):
- try:
- for lm_link in self.req('lun-maps')['lun-maps']:
- idx = lm_link['href'].split('/')[-1]
- lm = self.req('lun-maps', idx=int(idx))['content']
- if lm['ig-name'] == ig_name and lm['vol-name'] == vol_name:
- return lm
- except exception.NotFound:
- raise (exception.VolumeDriverException
- (_("can't find lunmap, ig:%(ig)s vol:%(vol)s") %
- {'ig': ig_name, 'vol': vol_name}))
-
- def _num_of_mapped_volumes(self, initiator):
- cnt = 0
- for lm_link in self.req('lun-maps')['lun-maps']:
- idx = lm_link['href'].split('/')[-1]
- lm = self.req('lun-maps', idx=int(idx))['content']
- if lm['ig-name'] == initiator:
- cnt += 1
- return cnt
-
def _get_password(self):
- return ''.join(self.random.choice
+ return ''.join(RANDOM.choice
(string.ascii_uppercase + string.digits)
for _ in range(12))
def create_lun_map(self, volume, ig):
try:
- res = self.req('lun-maps', 'POST', {'ig-id': ig['ig-id'][2],
- 'vol-id': volume['id']})
+ res = self.client.req('lun-maps', 'POST',
+ {'ig-id': ig['ig-id'][2],
+ 'vol-id': volume['id']})
lunmap = self._obj_from_result(res)
LOG.info(_LI('created lunmap\n%s'), lunmap)
- except urllib2.HTTPError as exc:
- if exc.code == 400:
- error = json.load(exc)
- if 'already_mapped' in error.message:
- LOG.info(_LI('volume already mapped,'
- ' trying to retrieve it %(ig)s, %(vol)d'),
- {'ig': ig['ig-id'][1], 'vol': volume['id']})
- lunmap = self._find_lunmap(ig['ig-id'][1], volume['id'])
- elif error.message == 'vol_obj_not_found':
- LOG.error(_LE("Can't find volume to map %s"), volume['id'])
- raise exception.VolumeNotFound(volume_id=volume['id'])
- else:
- raise
- else:
- raise
+ except exception.XtremIOAlreadyMappedError:
+ LOG.info(_LI('volume already mapped,'
+ ' trying to retrieve it %(ig)s, %(vol)d'),
+ {'ig': ig['ig-id'][1], 'vol': volume['id']})
+ lunmap = self.client.find_lunmap(ig['ig-id'][1], volume['id'])
return lunmap
def _get_ig(self, connector):
self.protocol = 'iSCSI'
def initialize_connection(self, volume, connector):
- # FIXME(shay-halsband): query the cluster index instead of using
- # the 1st one
try:
- sys = self.req('clusters', idx=1)['content']
+ sys = self.client.get_cluster()
except exception.NotFound:
msg = _("XtremIO not initialized correctly, no clusters found")
raise exception.VolumeBackendAPIException(data=msg)
initiator = self._get_initiator(connector)
try:
# check if the IG already exists
- ig = self.req('initiator-groups', 'GET',
- name=self._get_ig(connector))['content']
+ ig = self.client.req('initiator-groups', 'GET',
+ name=self._get_ig(connector))['content']
except exception.NotFound:
# create an initiator group to hold the the initiator
data = {'ig-name': self._get_ig(connector)}
- self.req('initiator-groups', 'POST', data)
+ self.client.req('initiator-groups', 'POST', data)
try:
- ig = self.req('initiator-groups',
- name=self._get_ig(connector))['content']
+ ig = self.client.req('initiator-groups',
+ name=self._get_ig(connector))['content']
except exception.NotFound:
raise (exception.VolumeBackendAPIException
(data=_("Failed to create IG, %s") %
self._get_ig(connector)))
try:
- init = self.req('initiators', 'GET',
- name=initiator)['content']
+ init = self.client.req('initiators', 'GET',
+ name=initiator)['content']
if use_chap:
chap_passwd = init['chap-authentication-initiator-'
'password']
if not chap_passwd:
LOG.info(_LI('initiator has no password while using chap,'
'removing it'))
- self.req('initiators', 'DELETE', name=initiator)
+ self.client.req('initiators', 'DELETE', name=initiator)
# check if the initiator already exists
raise exception.NotFound()
except exception.NotFound:
data['initiator-discovery-user-name'] = 'chap_user'
data['initiator-discovery-'
'password'] = self._get_password()
- self.req('initiators', 'POST', data)
+ self.client.req('initiators', 'POST', data)
# lun mappping
lunmap = self.create_lun_map(volume, ig)
:access_mode: the volume access mode allow client used
('rw' or 'ro' currently supported)
"""
- iscsi_portals = [t['name'] for t in self.req('iscsi-portals')
- ['iscsi-portals']]
- # get a random portal
- portal_name = self.random.choice(iscsi_portals)
- try:
- portal = self.req('iscsi-portals',
- name=portal_name)['content']
- except exception.NotFound:
- raise (exception.VolumeBackendAPIException
- (data=_("iscsi portal, %s, not found") % portal_name))
+ portal = self.client.get_iscsi_portal()
ip = portal['ip-addr'].split('/')[0]
properties = {'target_discovered': False,
'target_iqn': portal['port-address'],
def __init__(self, *args, **kwargs):
super(XtremIOFibreChannelDriver, self).__init__(*args, **kwargs)
self.protocol = 'FC'
+ self._targets = None
def get_targets(self):
- if not hasattr(self, '_targets'):
+ if not self._targets:
try:
- target_list = self.req('targets')["targets"]
- targets = [self.req('targets', name=target['name'])['content']
+ target_list = self.client.req('targets')["targets"]
+ targets = [self.client.req('targets',
+ name=target['name'])['content']
for target in target_list
if '-fc' in target['name']]
self._targets = [target['port-address'].replace(':', '')
# get or create initiator group
try:
# check if the IG already exists
- ig = self.req('initiator-groups', name=ig_name)['content']
+ ig = self.client.req('initiator-groups', name=ig_name)['content']
except exception.NotFound:
# create an initiator group to hold the the initiator
data = {'ig-name': ig_name}
- self.req('initiator-groups', 'POST', data)
+ self.client.req('initiator-groups', 'POST', data)
try:
- ig = self.req('initiator-groups', name=ig_name)['content']
+ ig = self.client.req('initiator-groups',
+ name=ig_name)['content']
except exception.NotFound:
raise (exception.VolumeBackendAPIException
(data=_("Failed to create IG, %s") % ig_name))
# get or create all initiators
for initiator in initiators:
try:
- self.req('initiators', name=initiator)['content']
+ self.client.req('initiators', name=initiator)['content']
except exception.NotFound:
# create an initiator
data = {'initiator-name': initiator,
'ig-id': ig['name'],
'port-address': initiator}
- self.req('initiators', 'POST', data)
+ self.client.req('initiators', 'POST', data)
i_t_map[initiator] = self.get_targets()
lunmap = self.create_lun_map(volume, ig)
def terminate_connection(self, volume, connector, **kwargs):
(super(XtremIOFibreChannelDriver, self)
.terminate_connection(volume, connector, **kwargs))
- num_vols = self._num_of_mapped_volumes(self._get_ig(connector))
+ num_vols = self.client.num_of_mapped_volumes(self._get_ig(connector))
if num_vols > 0:
data = {}
else: