XtremIO cinder iSCSI & FC volume drivers for Juno
XtremIO Direct Driver has been contributed to Juno release. support all minimum required features by Juno for both iSCSI and FibreChannel Certificate Test Results https://bugs.launchpad.net/cinder/+bug/1336844 Implements: blueprint xtremio-cinder-volume-driver Change-Id: I93ade3064532ec60662e92c56e5fefd4035a0d5b
This commit is contained in:
parent
9bddf33f9f
commit
6a0597c874
|
@ -0,0 +1,307 @@
|
||||||
|
# Copyright (c) 2012 - 2014 EMC Corporation, Inc.
|
||||||
|
# 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 mock
|
||||||
|
|
||||||
|
from cinder import exception
|
||||||
|
from cinder.openstack.common import log as logging
|
||||||
|
from cinder import test
|
||||||
|
from cinder.volume.drivers.emc.xtremio import XtremIOFibreChannelDriver
|
||||||
|
from cinder.volume.drivers.emc.xtremio import XtremIOISCSIDriver
|
||||||
|
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
typ2id = {'volumes': 'vol-id',
|
||||||
|
'snapshots': 'vol-id',
|
||||||
|
'initiators': 'initiator-id',
|
||||||
|
'initiator-groups': 'ig-id',
|
||||||
|
'lun-maps': 'mapping-id'}
|
||||||
|
|
||||||
|
xms_data = {'clusters': {1: {'sys-sw-version': "2.4.0-devel_ba23ee5381eeab73",
|
||||||
|
'chap-authentication-mode': 'disabled',
|
||||||
|
'chap-discovery-mode': 'disabled',
|
||||||
|
"index": 1}},
|
||||||
|
'target-groups': {'Default': {"index": 1, }},
|
||||||
|
'iscsi-portals': {'10.205.68.5/16':
|
||||||
|
{"port-address":
|
||||||
|
"iqn.2008-05.com.xtremio:001e67939c34",
|
||||||
|
"ip-port": 3260,
|
||||||
|
"ip-addr": "10.205.68.5/16",
|
||||||
|
"name": "10.205.68.5/16",
|
||||||
|
"index": 1}},
|
||||||
|
'targets': {'X1-SC2-fc1': {'index': 1, "name": "X1-SC2-fc1",
|
||||||
|
"port-address":
|
||||||
|
"21:00:00:24:ff:57:b2:36",
|
||||||
|
'port-state': 'up'},
|
||||||
|
'X1-SC2-fc2': {'index': 2, "name": "X1-SC2-fc2",
|
||||||
|
"port-address":
|
||||||
|
"21:00:00:24:ff:57:b2:55",
|
||||||
|
'port-state': 'up'}
|
||||||
|
},
|
||||||
|
'volumes': {},
|
||||||
|
'initiator-groups': {},
|
||||||
|
'initiators': {},
|
||||||
|
'lun-maps': {},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def clean_xms_data():
|
||||||
|
xms_data['volumes'] = {}
|
||||||
|
xms_data['initiator-groups'] = {}
|
||||||
|
xms_data['initiators'] = {}
|
||||||
|
xms_data['lun-maps'] = {}
|
||||||
|
|
||||||
|
|
||||||
|
def fix_data(data, object_type):
|
||||||
|
d = {}
|
||||||
|
for key, value in data.items():
|
||||||
|
if 'name' in key:
|
||||||
|
key = 'name'
|
||||||
|
d[key] = value
|
||||||
|
|
||||||
|
if object_type == 'lun-maps':
|
||||||
|
d['lun'] = 1
|
||||||
|
|
||||||
|
d[typ2id[object_type]] = ["a91e8c81c2d14ae4865187ce4f866f8a",
|
||||||
|
d.get('name'),
|
||||||
|
len(xms_data[object_type]) + 1]
|
||||||
|
d['index'] = len(xms_data[object_type]) + 1
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
def get_xms_obj_key(data):
|
||||||
|
for key in data.keys():
|
||||||
|
if 'name' in key:
|
||||||
|
return key
|
||||||
|
|
||||||
|
|
||||||
|
def xms_request(object_type='volumes', request_typ='GET', data=None,
|
||||||
|
name=None, idx=None):
|
||||||
|
if object_type == 'snapshots':
|
||||||
|
object_type = 'volumes'
|
||||||
|
|
||||||
|
obj_key = name if name else idx
|
||||||
|
if request_typ == 'GET':
|
||||||
|
res = xms_data[object_type]
|
||||||
|
if name or idx:
|
||||||
|
if obj_key not in res:
|
||||||
|
raise exception.NotFound()
|
||||||
|
return {"content": res[obj_key]}
|
||||||
|
else:
|
||||||
|
return {object_type: [{"href": "/%s/%d" % (object_type,
|
||||||
|
obj['index']),
|
||||||
|
"name": obj.get('name')}
|
||||||
|
for obj in res.values()]}
|
||||||
|
elif request_typ == 'POST':
|
||||||
|
data = fix_data(data, object_type)
|
||||||
|
data['index'] = len(xms_data[object_type]) + 1
|
||||||
|
xms_data[object_type][data['index']] = data
|
||||||
|
# find the name key
|
||||||
|
name_key = get_xms_obj_key(data)
|
||||||
|
if object_type == 'lun-maps':
|
||||||
|
data['ig-name'] = data['ig-id']
|
||||||
|
if name_key:
|
||||||
|
if data[name_key] in xms_data[object_type]:
|
||||||
|
raise (exception
|
||||||
|
.VolumeBackendAPIException
|
||||||
|
('Volume by this name already exists'))
|
||||||
|
xms_data[object_type][data[name_key]] = data
|
||||||
|
|
||||||
|
return {"links": [{"href": "/%s/%d" %
|
||||||
|
(object_type, data[typ2id[object_type]][2])}]}
|
||||||
|
elif request_typ == 'DELETE':
|
||||||
|
if obj_key in xms_data[object_type]:
|
||||||
|
data = xms_data[object_type][obj_key]
|
||||||
|
del xms_data[object_type][data['index']]
|
||||||
|
del xms_data[object_type][data[typ2id[object_type]][1]]
|
||||||
|
else:
|
||||||
|
LOG.error('trying to delete a missing object %s', str(obj_key))
|
||||||
|
raise exception.NotFound()
|
||||||
|
elif request_typ == 'PUT':
|
||||||
|
if obj_key in xms_data[object_type]:
|
||||||
|
obj = xms_data[object_type][obj_key]
|
||||||
|
obj.update(data)
|
||||||
|
else:
|
||||||
|
LOG.error('trying to update a missing object %s', str(obj_key))
|
||||||
|
raise exception.NotFound()
|
||||||
|
|
||||||
|
|
||||||
|
def xms_bad_request(object_type='volumes', request_typ='GET', data=None,
|
||||||
|
name=None, idx=None):
|
||||||
|
if request_typ == 'GET':
|
||||||
|
raise exception.NotFound()
|
||||||
|
elif request_typ == 'POST':
|
||||||
|
raise exception.VolumeBackendAPIException('failed to create ig')
|
||||||
|
|
||||||
|
|
||||||
|
class D(dict):
|
||||||
|
def update(self, *args, **kwargs):
|
||||||
|
self.__dict__.update(*args, **kwargs)
|
||||||
|
return dict.update(self, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class CommonData():
|
||||||
|
connector = {'ip': '10.0.0.2',
|
||||||
|
'initiator': 'iqn.1993-08.org.debian:01:222',
|
||||||
|
'wwpns': ["123456789012345", "123456789054321"],
|
||||||
|
'wwnns': ["223456789012345", "223456789054321"],
|
||||||
|
'host': 'fakehost'}
|
||||||
|
|
||||||
|
test_volume = {'name': 'vol1',
|
||||||
|
'size': 1,
|
||||||
|
'volume_name': 'vol1',
|
||||||
|
'id': '192eb39b-6c2f-420c-bae3-3cfd117f0001',
|
||||||
|
'provider_auth': None,
|
||||||
|
'project_id': 'project',
|
||||||
|
'display_name': 'vol1',
|
||||||
|
'display_description': 'test volume',
|
||||||
|
'volume_type_id': None}
|
||||||
|
test_snapshot = D()
|
||||||
|
test_snapshot.update({'name': 'snapshot1',
|
||||||
|
'size': 1,
|
||||||
|
'id': '192eb39b-6c2f-420c-bae3-3cfd117f0002',
|
||||||
|
'volume_name': 'vol-vol1',
|
||||||
|
'volume_id': '192eb39b-6c2f-420c-bae3-3cfd117f0001',
|
||||||
|
'project_id': 'project'})
|
||||||
|
test_snapshot.__dict__.update(test_snapshot)
|
||||||
|
test_volume2 = {'name': 'vol2',
|
||||||
|
'size': 1,
|
||||||
|
'volume_name': 'vol2',
|
||||||
|
'id': '192eb39b-6c2f-420c-bae3-3cfd117f0004',
|
||||||
|
'provider_auth': None,
|
||||||
|
'project_id': 'project',
|
||||||
|
'display_name': 'vol2',
|
||||||
|
'display_description': 'test volume 2',
|
||||||
|
'volume_type_id': None}
|
||||||
|
test_clone = {'name': 'clone1',
|
||||||
|
'size': 1,
|
||||||
|
'volume_name': 'vol3',
|
||||||
|
'id': '192eb39b-6c2f-420c-bae3-3cfd117f0003',
|
||||||
|
'provider_auth': None,
|
||||||
|
'project_id': 'project',
|
||||||
|
'display_name': 'clone1',
|
||||||
|
'display_description': 'volume created from snapshot',
|
||||||
|
'volume_type_id': None}
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch('cinder.volume.drivers.emc.xtremio.XtremIOISCSIDriver.req')
|
||||||
|
class EMCXIODriverISCSITestCase(test.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(EMCXIODriverISCSITestCase, self).setUp()
|
||||||
|
|
||||||
|
configuration = mock.Mock()
|
||||||
|
configuration.san_login = ''
|
||||||
|
configuration.san_password = ''
|
||||||
|
configuration.san_ip = ''
|
||||||
|
self.driver = XtremIOISCSIDriver(configuration=configuration)
|
||||||
|
|
||||||
|
self.data = CommonData()
|
||||||
|
|
||||||
|
def test_create_extend_delete_volume(self, req):
|
||||||
|
req.side_effect = xms_request
|
||||||
|
clean_xms_data()
|
||||||
|
self.driver.create_volume(self.data.test_volume)
|
||||||
|
self.driver.extend_volume(self.data.test_volume, 5)
|
||||||
|
self.driver.delete_volume(self.data.test_volume)
|
||||||
|
|
||||||
|
def test_create_delete_snapshot(self, req):
|
||||||
|
req.side_effect = xms_request
|
||||||
|
clean_xms_data()
|
||||||
|
self.driver.create_volume(self.data.test_volume)
|
||||||
|
self.driver.create_snapshot(self.data.test_snapshot)
|
||||||
|
self.driver.delete_snapshot(self.data.test_snapshot)
|
||||||
|
self.driver.delete_volume(self.data.test_volume)
|
||||||
|
|
||||||
|
def test_volume_from_snapshot(self, req):
|
||||||
|
req.side_effect = xms_request
|
||||||
|
clean_xms_data()
|
||||||
|
xms_data['volumes'] = {}
|
||||||
|
self.driver.create_volume(self.data.test_volume)
|
||||||
|
self.driver.create_snapshot(self.data.test_snapshot)
|
||||||
|
self.driver.create_volume_from_snapshot(self.data.test_volume2,
|
||||||
|
self.data.test_snapshot)
|
||||||
|
self.driver.delete_volume(self.data.test_volume2)
|
||||||
|
self.driver.delete_volume(self.data.test_snapshot)
|
||||||
|
self.driver.delete_volume(self.data.test_volume)
|
||||||
|
|
||||||
|
def test_clone_volume(self, req):
|
||||||
|
req.side_effect = xms_request
|
||||||
|
clean_xms_data()
|
||||||
|
self.driver.create_volume(self.data.test_volume)
|
||||||
|
self.driver.create_cloned_volume(self.data.test_clone,
|
||||||
|
self.data.test_volume)
|
||||||
|
self.driver.delete_volume(self.data.test_clone)
|
||||||
|
self.driver.delete_volume(self.data.test_volume)
|
||||||
|
|
||||||
|
def test_duplicate_volume(self, req):
|
||||||
|
req.side_effect = xms_request
|
||||||
|
clean_xms_data()
|
||||||
|
self.driver.create_volume(self.data.test_volume)
|
||||||
|
self.assertRaises(exception.VolumeBackendAPIException,
|
||||||
|
self.driver.create_volume, self.data.test_volume)
|
||||||
|
self.driver.delete_volume(self.data.test_volume)
|
||||||
|
|
||||||
|
def test_initialize_terminate_connection(self, req):
|
||||||
|
req.side_effect = xms_request
|
||||||
|
clean_xms_data()
|
||||||
|
self.driver.create_volume(self.data.test_volume)
|
||||||
|
map_data = self.driver.initialize_connection(self.data.test_volume,
|
||||||
|
self.data.connector)
|
||||||
|
self.assertEqual(map_data['data']['target_lun'], 1)
|
||||||
|
self.driver.terminate_connection(self.data.test_volume,
|
||||||
|
self.data.connector)
|
||||||
|
|
||||||
|
def test_initialize_connection_bad_ig(self, req):
|
||||||
|
req.side_effect = xms_bad_request
|
||||||
|
clean_xms_data()
|
||||||
|
self.assertRaises(exception.VolumeBackendAPIException,
|
||||||
|
self.driver.initialize_connection,
|
||||||
|
self.data.test_volume,
|
||||||
|
self.data.connector)
|
||||||
|
self.driver.delete_volume(self.data.test_volume)
|
||||||
|
|
||||||
|
def test_get_stats(self, req):
|
||||||
|
req.side_effect = xms_request
|
||||||
|
clean_xms_data()
|
||||||
|
stats = self.driver.get_volume_stats(True)
|
||||||
|
self.assertEqual(stats['volume_backend_name'],
|
||||||
|
self.driver.backend_name)
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch('cinder.volume.drivers.emc.xtremio.XtremIOFibreChannelDriver.req')
|
||||||
|
class EMCXIODriverFibreChannelTestCase(test.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(EMCXIODriverFibreChannelTestCase, self).setUp()
|
||||||
|
|
||||||
|
configuration = mock.Mock()
|
||||||
|
configuration.san_login = ''
|
||||||
|
configuration.san_password = ''
|
||||||
|
configuration.san_ip = ''
|
||||||
|
self.driver = XtremIOFibreChannelDriver(configuration=configuration)
|
||||||
|
|
||||||
|
self.data = CommonData()
|
||||||
|
|
||||||
|
def test_initialize_terminate_connection(self, req):
|
||||||
|
req.side_effect = xms_request
|
||||||
|
clean_xms_data()
|
||||||
|
self.driver.create_volume(self.data.test_volume)
|
||||||
|
map_data = self.driver.initialize_connection(self.data.test_volume,
|
||||||
|
self.data.connector)
|
||||||
|
self.assertEqual(map_data['data']['target_lun'], 1)
|
||||||
|
self.driver.terminate_connection(self.data.test_volume,
|
||||||
|
self.data.connector)
|
||||||
|
self.driver.delete_volume(self.data.test_volume)
|
|
@ -0,0 +1,509 @@
|
||||||
|
# Copyright (c) 2012 - 2014 EMC Corporation.
|
||||||
|
# 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.
|
||||||
|
"""
|
||||||
|
Driver for EMC XtremIO Storage.
|
||||||
|
supported XtremIO version 2.4 and up
|
||||||
|
|
||||||
|
1.0.0 - initial release
|
||||||
|
1.0.1 - enable volume extend
|
||||||
|
1.0.2 - added FC support, improved error handling
|
||||||
|
1.0.3 - update logging level, add translation
|
||||||
|
1.0.4 - support for FC zones
|
||||||
|
"""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
import urllib
|
||||||
|
import urllib2
|
||||||
|
|
||||||
|
import six
|
||||||
|
|
||||||
|
from cinder import exception
|
||||||
|
from cinder.i18n import _
|
||||||
|
from cinder.openstack.common import log as logging
|
||||||
|
from cinder.volume import driver
|
||||||
|
from cinder.volume.drivers.san import san
|
||||||
|
from cinder.zonemanager import utils as fczm_utils
|
||||||
|
|
||||||
|
|
||||||
|
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))
|
||||||
|
.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)
|
||||||
|
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
|
||||||
|
request.add_header("Authorization", "Basic %s" % (self.base64_auth, ))
|
||||||
|
return request
|
||||||
|
|
||||||
|
def _send_request(self, object_type, key, request):
|
||||||
|
try:
|
||||||
|
response = urllib2.urlopen(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'):
|
||||||
|
LOG.warning(_("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':
|
||||||
|
LOG.error(_("can't create 2 volumes with the same name"))
|
||||||
|
msg = (_('Volume by this name already exists'))
|
||||||
|
raise exception.VolumeBackendAPIException(data=msg)
|
||||||
|
LOG.error(_('Bad response from XMS, %s'), exc.read())
|
||||||
|
msg = (_('Exception: %s') % six.text_type(exc))
|
||||||
|
raise exception.VolumeDriverException(message=msg)
|
||||||
|
if response.code >= 300:
|
||||||
|
LOG.error(_('bad API response, %s'), response.msg)
|
||||||
|
msg = (_('bad response from XMS got http code %(code)d, %(msg)s') %
|
||||||
|
{'code': response.code, 'msg': response.msg})
|
||||||
|
raise exception.VolumeBackendAPIException(data=msg)
|
||||||
|
return response
|
||||||
|
|
||||||
|
def req(self, object_type='volumes', request_typ='GET', data=None,
|
||||||
|
name=None, idx=None):
|
||||||
|
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)
|
||||||
|
key = None
|
||||||
|
if name:
|
||||||
|
url = '%s?%s' % (url, urllib.urlencode({'name': name}))
|
||||||
|
key = name
|
||||||
|
elif idx:
|
||||||
|
url = '%s/%d' % (url, idx)
|
||||||
|
key = str(idx)
|
||||||
|
request = self._create_request(request_typ, data, url, urllib)
|
||||||
|
response = self._send_request(object_type, key, request)
|
||||||
|
str_result = response.read()
|
||||||
|
if str_result:
|
||||||
|
try:
|
||||||
|
return json.loads(str_result)
|
||||||
|
except Exception:
|
||||||
|
LOG.exception(_('querying %(typ)s, %(req)s failed to '
|
||||||
|
'parse result, return value = %(res)s'),
|
||||||
|
{'typ': object_type,
|
||||||
|
'req': request_typ,
|
||||||
|
'res': str_result})
|
||||||
|
|
||||||
|
def _obj_from_result(self, res):
|
||||||
|
typ, idx = res['links'][0]['href'].split('/')[-2:]
|
||||||
|
return self.req(typ, idx=int(idx))['content']
|
||||||
|
|
||||||
|
def check_for_setup_error(self):
|
||||||
|
try:
|
||||||
|
sys = self.req('clusters', idx=1)['content']
|
||||||
|
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('.')]
|
||||||
|
if ver < self.MIN_XMS_VERSION:
|
||||||
|
msg = _('Invalid XtremIO version %s,'
|
||||||
|
' version 2.4 or up is required') % sys['sys-sw-version']
|
||||||
|
LOG.error(msg)
|
||||||
|
raise exception.VolumeBackendAPIException(data=msg)
|
||||||
|
else:
|
||||||
|
LOG.info(_('XtremIO SW version %s'), sys['sys-sw-version'])
|
||||||
|
|
||||||
|
def create_volume(self, volume):
|
||||||
|
"Creates a volume"
|
||||||
|
data = {'vol-name': volume['id'],
|
||||||
|
'vol-size': str(volume['size']) + 'g'
|
||||||
|
}
|
||||||
|
|
||||||
|
self.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)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
def delete_volume(self, volume):
|
||||||
|
"""Deletes a volume."""
|
||||||
|
try:
|
||||||
|
self.req('volumes', 'DELETE', name=volume['id'])
|
||||||
|
except exception.NotFound:
|
||||||
|
LOG.info(_("volume %s doesn't exist"), volume['id'])
|
||||||
|
|
||||||
|
def create_snapshot(self, snapshot):
|
||||||
|
"""Creates a snapshot."""
|
||||||
|
data = {'snap-vol-name': snapshot.id,
|
||||||
|
'ancestor-vol-id': snapshot.volume_id}
|
||||||
|
|
||||||
|
self.req('snapshots', 'POST', data)
|
||||||
|
|
||||||
|
def delete_snapshot(self, snapshot):
|
||||||
|
"""Deletes a snapshot."""
|
||||||
|
try:
|
||||||
|
self.req('volumes', 'DELETE', name=snapshot.id)
|
||||||
|
except exception.NotFound:
|
||||||
|
LOG.info(_("snapshot %s doesn't exist"), snapshot.id)
|
||||||
|
|
||||||
|
def _update_volume_stats(self):
|
||||||
|
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,
|
||||||
|
'QoS_support': False}
|
||||||
|
|
||||||
|
def get_volume_stats(self, refresh=False):
|
||||||
|
"""Get volume stats.
|
||||||
|
If 'refresh' is True, run update the stats first.
|
||||||
|
"""
|
||||||
|
if refresh:
|
||||||
|
self._update_volume_stats()
|
||||||
|
return self._stats
|
||||||
|
|
||||||
|
def extend_volume(self, volume, new_size):
|
||||||
|
"""Extend an existing volume's size."""
|
||||||
|
data = {'vol-size': str(new_size) + 'g'}
|
||||||
|
try:
|
||||||
|
self.req('volumes', 'PUT', data, name=volume['id'])
|
||||||
|
except exception.NotFound:
|
||||||
|
msg = _("can't find the volume to extend")
|
||||||
|
raise (exception.VolumeDriverException(message=msg))
|
||||||
|
|
||||||
|
def check_for_export(self, context, volume_id):
|
||||||
|
"""Make sure volume is exported."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
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(_('removing lun map %s'), lm_name)
|
||||||
|
self.req('lun-maps', 'DELETE', name=lm_name)
|
||||||
|
except exception.NotFound:
|
||||||
|
LOG.warning(_("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
|
||||||
|
(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']})
|
||||||
|
lunmap = self._obj_from_result(res)
|
||||||
|
LOG.info(_('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(_('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(_("Can't find volume to map %s"), volume['id'])
|
||||||
|
raise exception.VolumeNotFound(volume_id=volume['id'])
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
else:
|
||||||
|
raise
|
||||||
|
return lunmap
|
||||||
|
|
||||||
|
def _get_ig(self, connector):
|
||||||
|
raise NotImplementedError()
|
||||||
|
|
||||||
|
|
||||||
|
class XtremIOISCSIDriver(XtremIOVolumeDriver, driver.ISCSIDriver):
|
||||||
|
"""Executes commands relating to ISCSI volumes.
|
||||||
|
|
||||||
|
We make use of model provider properties as follows:
|
||||||
|
|
||||||
|
``provider_location``
|
||||||
|
if present, contains the iSCSI target information in the same
|
||||||
|
format as an ietadm discovery
|
||||||
|
i.e. '<ip>:<port>,<portal> <target IQN>'
|
||||||
|
|
||||||
|
``provider_auth``
|
||||||
|
if present, contains a space-separated triple:
|
||||||
|
'<auth method> <auth username> <auth password>'.
|
||||||
|
`CHAP` is the only auth_method in use at the moment.
|
||||||
|
"""
|
||||||
|
driver_name = 'XtremIO_ISCSI'
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(XtremIOISCSIDriver, self).__init__(*args, **kwargs)
|
||||||
|
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']
|
||||||
|
except exception.NotFound:
|
||||||
|
msg = _("XtremIO not initialized correctly, no clusters found")
|
||||||
|
raise exception.VolumeBackendAPIException(data=msg)
|
||||||
|
use_chap = (sys.get('chap-authentication-mode', 'disabled') !=
|
||||||
|
'disabled')
|
||||||
|
discovery_chap = (sys.get('chap-discovery-mode', 'disabled') !=
|
||||||
|
'disabled')
|
||||||
|
initiator = self._get_initiator(connector)
|
||||||
|
try:
|
||||||
|
# check if the IG already exists
|
||||||
|
ig = self.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)
|
||||||
|
try:
|
||||||
|
ig = self.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']
|
||||||
|
if use_chap:
|
||||||
|
chap_passwd = init['chap-authentication-initiator-'
|
||||||
|
'password']
|
||||||
|
# delete the initiator to create a new one with password
|
||||||
|
if not chap_passwd:
|
||||||
|
LOG.info(_('initiator has no password while using chap,'
|
||||||
|
'removing it'))
|
||||||
|
self.req('initiators', 'DELETE', name=initiator)
|
||||||
|
# check if the initiator already exists
|
||||||
|
raise exception.NotFound()
|
||||||
|
except exception.NotFound:
|
||||||
|
# create an initiator
|
||||||
|
data = {'initiator-name': initiator,
|
||||||
|
'ig-id': initiator,
|
||||||
|
'port-address': initiator}
|
||||||
|
if use_chap:
|
||||||
|
data['initiator-authentication-user-name'] = 'chap_user'
|
||||||
|
chap_passwd = self._get_password()
|
||||||
|
data['initiator-authentication-password'] = chap_passwd
|
||||||
|
if discovery_chap:
|
||||||
|
data['initiator-discovery-user-name'] = 'chap_user'
|
||||||
|
data['initiator-discovery-'
|
||||||
|
'password'] = self._get_password()
|
||||||
|
self.req('initiators', 'POST', data)
|
||||||
|
# lun mappping
|
||||||
|
lunmap = self.create_lun_map(volume, ig)
|
||||||
|
|
||||||
|
properties = self._get_iscsi_properties(lunmap)
|
||||||
|
|
||||||
|
if use_chap:
|
||||||
|
properties['auth_method'] = 'CHAP'
|
||||||
|
properties['auth_username'] = 'chap_user'
|
||||||
|
properties['auth_password'] = chap_passwd
|
||||||
|
|
||||||
|
LOG.debug('init conn params:\n%s', properties)
|
||||||
|
return {
|
||||||
|
'driver_volume_type': 'iscsi',
|
||||||
|
'data': properties
|
||||||
|
}
|
||||||
|
|
||||||
|
def _get_iscsi_properties(self, lunmap):
|
||||||
|
"""Gets iscsi configuration
|
||||||
|
:target_discovered: boolean indicating whether discovery was used
|
||||||
|
:target_iqn: the IQN of the iSCSI target
|
||||||
|
:target_portal: the portal of the iSCSI target
|
||||||
|
:target_lun: the lun of the iSCSI target
|
||||||
|
:volume_id: the id of the volume (currently used by xen)
|
||||||
|
:auth_method:, :auth_username:, :auth_password:
|
||||||
|
the authentication details. Right now, either auth_method is not
|
||||||
|
present meaning no authentication, or auth_method == `CHAP`
|
||||||
|
meaning use CHAP with the specified credentials.
|
||||||
|
: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))
|
||||||
|
ip = portal['ip-addr'].split('/')[0]
|
||||||
|
properties = {'target_discovered': False,
|
||||||
|
'target_iqn': portal['port-address'],
|
||||||
|
'target_lun': lunmap['lun'],
|
||||||
|
'target_portal': '%s:%d' % (ip, portal['ip-port']),
|
||||||
|
'access_mode': 'rw'}
|
||||||
|
return properties
|
||||||
|
|
||||||
|
def _get_initiator(self, connector):
|
||||||
|
return connector['initiator']
|
||||||
|
|
||||||
|
def _get_ig(self, connector):
|
||||||
|
return connector['initiator']
|
||||||
|
|
||||||
|
|
||||||
|
class XtremIOFibreChannelDriver(XtremIOVolumeDriver,
|
||||||
|
driver.FibreChannelDriver):
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(XtremIOFibreChannelDriver, self).__init__(*args, **kwargs)
|
||||||
|
self.protocol = 'FC'
|
||||||
|
|
||||||
|
def get_targets(self):
|
||||||
|
if not hasattr(self, '_targets'):
|
||||||
|
try:
|
||||||
|
target_list = self.req('targets')["targets"]
|
||||||
|
targets = [self.req('targets', name=target['name'])['content']
|
||||||
|
for target in target_list
|
||||||
|
if '-fc' in target['name']]
|
||||||
|
self._targets = [target['port-address'].replace(':', '')
|
||||||
|
for target in targets
|
||||||
|
if target['port-state'] == 'up']
|
||||||
|
except exception.NotFound:
|
||||||
|
raise (exception.VolumeBackendAPIException
|
||||||
|
(data=_("Failed to get targets")))
|
||||||
|
return self._targets
|
||||||
|
|
||||||
|
@fczm_utils.AddFCZone
|
||||||
|
def initialize_connection(self, volume, connector):
|
||||||
|
initiators = self._get_initiator(connector)
|
||||||
|
ig_name = self._get_ig(connector)
|
||||||
|
i_t_map = {}
|
||||||
|
# get or create initiator group
|
||||||
|
try:
|
||||||
|
# check if the IG already exists
|
||||||
|
ig = self.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)
|
||||||
|
try:
|
||||||
|
ig = self.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']
|
||||||
|
except exception.NotFound:
|
||||||
|
# create an initiator
|
||||||
|
data = {'initiator-name': initiator,
|
||||||
|
'ig-id': ig['name'],
|
||||||
|
'port-address': initiator}
|
||||||
|
self.req('initiators', 'POST', data)
|
||||||
|
i_t_map[initiator] = self.get_targets()
|
||||||
|
|
||||||
|
lunmap = self.create_lun_map(volume, ig)
|
||||||
|
return {'driver_volume_type': 'fibre_channel',
|
||||||
|
'data': {
|
||||||
|
'target_discovered': True,
|
||||||
|
'target_lun': lunmap['lun'],
|
||||||
|
'target_wwn': self.get_targets(),
|
||||||
|
'access_mode': 'rw',
|
||||||
|
'initiator_target_map': i_t_map}}
|
||||||
|
|
||||||
|
@fczm_utils.RemoveFCZone
|
||||||
|
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))
|
||||||
|
if num_vols > 0:
|
||||||
|
data = {}
|
||||||
|
else:
|
||||||
|
i_t_map = {}
|
||||||
|
for initiator in self._get_initiator(connector):
|
||||||
|
i_t_map[initiator] = self.get_targets()
|
||||||
|
data = {'target_wwn': self.get_targets(),
|
||||||
|
'initiator_target_map': i_t_map}
|
||||||
|
|
||||||
|
return {'driver_volume_type': 'fibre_channel',
|
||||||
|
'data': data}
|
||||||
|
|
||||||
|
def _get_initiator(self, connector):
|
||||||
|
return connector['wwpns']
|
||||||
|
|
||||||
|
def _get_ig(self, connector):
|
||||||
|
return connector['host']
|
Loading…
Reference in New Issue