Merge changes from kilo version of the dothill driver
- Add support for management controller failover - Allow longer initiator nicknames if not running titanium firmware - Handle race conditions in choosing next available LUN and creating initiator nicknames - Serialize access to the array to avoid overloading it - Add timeouts for HTTP(S) requests - Reuse array API session keys to reduce array overhead - Handle missing wwpns property in terminate_connection - Fix behavior of extend_volume() and methods that call it - Change units from GB to GiB when creating volumes - Update test cases Change-Id: Idf31e127eaaa360f10b2dd0ce7820c55416f8fbc Closes-Bug: #1554770 Closes-Bug: #1560646
This commit is contained in:
parent
75b89a36e3
commit
65b97f6743
@ -1,5 +1,6 @@
|
||||
# Copyright 2014 Objectif Libre
|
||||
# Copyright 2015 DotHill Systems
|
||||
# Copyright 2016 Seagate Technology or one of its affiliates
|
||||
#
|
||||
# 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
|
||||
@ -34,6 +35,10 @@ resp_login = '''<RESPONSE><OBJECT basetype="status" name="status" oid="1">
|
||||
<PROPERTY name="response-type-numeric">0</PROPERTY>
|
||||
<PROPERTY name="response">12a1626754554a21d85040760c81b</PROPERTY>
|
||||
<PROPERTY name="return-code">1</PROPERTY></OBJECT></RESPONSE>'''
|
||||
|
||||
resp_fw = '''<RESPONSE><PROPERTY name="sc-fw">GLS220R001</PROPERTY>
|
||||
<PROPERTY name="return-code">0</PROPERTY></RESPONSE>'''
|
||||
|
||||
resp_badlogin = '''<RESPONSE><OBJECT basetype="status" name="status" oid="1">
|
||||
<PROPERTY name="response-type">error</PROPERTY>
|
||||
<PROPERTY name="response-type-numeric">1</PROPERTY>
|
||||
@ -107,13 +112,18 @@ test_host = {'capabilities': {'location_info':
|
||||
'DotHillVolumeDriver:xxxxx:dg02:A'}}
|
||||
test_snap = {'id': 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
|
||||
'volume': {'name_id': None},
|
||||
'volume_id': vol_id,
|
||||
'display_name': 'test volume', 'name': 'volume', 'size': 10}
|
||||
'volume_id': vol_id, 'display_name': 'test volume',
|
||||
'name': 'volume', 'volume_size': 10}
|
||||
encoded_volid = 'v_O7DDpi8TOWF_9cwnMF'
|
||||
encoded_snapid = 's_O7DDpi8TOWF_9cwnMF'
|
||||
dest_volume = {'id': 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
|
||||
'source_volid': vol_id,
|
||||
'display_name': 'test volume', 'name': 'volume', 'size': 10}
|
||||
dest_volume_larger = {'id': 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
|
||||
'name_id': None,
|
||||
'source_volid': vol_id,
|
||||
'display_name': 'test volume',
|
||||
'name': 'volume', 'size': 20}
|
||||
attached_volume = {'id': vol_id,
|
||||
'display_name': 'test volume', 'name': 'volume',
|
||||
'size': 10, 'status': 'in-use',
|
||||
@ -153,14 +163,16 @@ class TestDotHillClient(test.TestCase):
|
||||
@mock.patch('requests.get')
|
||||
def test_login(self, mock_requests_get):
|
||||
m = mock.Mock()
|
||||
m.text.encode.side_effect = [resp_login]
|
||||
mock_requests_get.return_value = m
|
||||
self.client.login()
|
||||
self.assertEqual(session_key, self.client._session_key)
|
||||
m.text.encode.side_effect = [resp_badlogin]
|
||||
|
||||
m.text.encode.side_effect = [resp_badlogin, resp_badlogin]
|
||||
self.assertRaises(exception.DotHillAuthenticationError,
|
||||
self.client.login)
|
||||
|
||||
m.text.encode.side_effect = [resp_login, resp_fw]
|
||||
self.client.login()
|
||||
self.assertEqual(session_key, self.client._session_key)
|
||||
|
||||
def test_build_request_url(self):
|
||||
url = self.client._build_request_url('/path')
|
||||
self.assertEqual('http://10.0.0.1/api/path', url)
|
||||
@ -185,13 +197,13 @@ class TestDotHillClient(test.TestCase):
|
||||
requests.exceptions.
|
||||
RequestException("error")]
|
||||
mock_requests_get.return_value = m
|
||||
ret = self.client._request('/path')
|
||||
ret = self.client._api_request('/path')
|
||||
self.assertTrue(type(ret) == etree._Element)
|
||||
self.assertRaises(exception.DotHillConnectionError,
|
||||
self.client._request,
|
||||
self.client._api_request,
|
||||
'/path')
|
||||
self.assertRaises(exception.DotHillConnectionError,
|
||||
self.client._request,
|
||||
self.client._api_request,
|
||||
'/path')
|
||||
|
||||
def test_assert_response_ok(self):
|
||||
@ -377,7 +389,7 @@ class TestFCDotHillCommon(test.TestCase):
|
||||
ret = self.common.create_volume(test_volume)
|
||||
self.assertIsNone(ret)
|
||||
mock_create.assert_called_with(encoded_volid,
|
||||
"%sGB" % test_volume['size'],
|
||||
"%sGiB" % test_volume['size'],
|
||||
self.common.backend_name,
|
||||
self.common.backend_type)
|
||||
|
||||
@ -419,7 +431,38 @@ class TestFCDotHillCommon(test.TestCase):
|
||||
|
||||
@mock.patch.object(dothill.DotHillClient, 'copy_volume')
|
||||
@mock.patch.object(dothill.DotHillClient, 'backend_stats')
|
||||
def test_create_volume_from_snapshot(self, mock_stats, mock_copy):
|
||||
@mock.patch.object(dothill_common.DotHillCommon, 'extend_volume')
|
||||
def test_create_cloned_volume_larger(self, mock_extend, mock_stats,
|
||||
mock_copy):
|
||||
mock_stats.side_effect = [stats_low_space, stats_large_space,
|
||||
stats_large_space]
|
||||
|
||||
self.assertRaises(exception.DotHillNotEnoughSpace,
|
||||
self.common.create_cloned_volume,
|
||||
dest_volume_larger, detached_volume)
|
||||
self.assertFalse(mock_copy.called)
|
||||
|
||||
mock_copy.side_effect = [exception.DotHillRequestError, None]
|
||||
self.assertRaises(exception.Invalid,
|
||||
self.common.create_cloned_volume,
|
||||
dest_volume_larger, detached_volume)
|
||||
|
||||
ret = self.common.create_cloned_volume(dest_volume_larger,
|
||||
detached_volume)
|
||||
self.assertIsNone(ret)
|
||||
mock_copy.assert_called_with(encoded_volid,
|
||||
'vqqqqqqqqqqqqqqqqqqq',
|
||||
self.common.backend_name,
|
||||
self.common.backend_type)
|
||||
mock_extend.assert_called_once_with(dest_volume_larger,
|
||||
dest_volume_larger['size'])
|
||||
|
||||
@mock.patch.object(dothill.DotHillClient, 'get_volume_size')
|
||||
@mock.patch.object(dothill.DotHillClient, 'extend_volume')
|
||||
@mock.patch.object(dothill.DotHillClient, 'copy_volume')
|
||||
@mock.patch.object(dothill.DotHillClient, 'backend_stats')
|
||||
def test_create_volume_from_snapshot(self, mock_stats, mock_copy,
|
||||
mock_extend, mock_get_size):
|
||||
mock_stats.side_effect = [stats_low_space, stats_large_space,
|
||||
stats_large_space]
|
||||
|
||||
@ -428,26 +471,30 @@ class TestFCDotHillCommon(test.TestCase):
|
||||
dest_volume, test_snap)
|
||||
|
||||
mock_copy.side_effect = [exception.DotHillRequestError, None]
|
||||
mock_get_size.return_value = test_snap['volume_size']
|
||||
self.assertRaises(exception.Invalid,
|
||||
self.common.create_volume_from_snapshot,
|
||||
dest_volume, test_snap)
|
||||
|
||||
ret = self.common.create_volume_from_snapshot(dest_volume, test_snap)
|
||||
ret = self.common.create_volume_from_snapshot(dest_volume_larger,
|
||||
test_snap)
|
||||
self.assertIsNone(ret)
|
||||
mock_copy.assert_called_with('sqqqqqqqqqqqqqqqqqqq',
|
||||
'vqqqqqqqqqqqqqqqqqqq',
|
||||
self.common.backend_name,
|
||||
self.common.backend_type)
|
||||
mock_extend.assert_called_with('vqqqqqqqqqqqqqqqqqqq', '10GiB')
|
||||
|
||||
@mock.patch.object(dothill.DotHillClient, 'get_volume_size')
|
||||
@mock.patch.object(dothill.DotHillClient, 'extend_volume')
|
||||
def test_extend_volume(self, mock_extend):
|
||||
def test_extend_volume(self, mock_extend, mock_size):
|
||||
mock_extend.side_effect = [exception.DotHillRequestError, None]
|
||||
|
||||
mock_size.side_effect = [10, 10]
|
||||
self.assertRaises(exception.Invalid, self.common.extend_volume,
|
||||
test_volume, 20)
|
||||
ret = self.common.extend_volume(test_volume, 20)
|
||||
self.assertIsNone(ret)
|
||||
mock_extend.assert_called_with(encoded_volid, '10GB')
|
||||
mock_extend.assert_called_with(encoded_volid, '10GiB')
|
||||
|
||||
@mock.patch.object(dothill.DotHillClient, 'create_snapshot')
|
||||
def test_create_snapshot(self, mock_create):
|
||||
@ -620,14 +667,12 @@ class TestDotHillFC(test.TestCase):
|
||||
@mock.patch.object(dothill_common.DotHillCommon, 'unmap_volume')
|
||||
@mock.patch.object(dothill.DotHillClient, 'list_luns_for_host')
|
||||
def test_terminate_connection(self, mock_list, mock_unmap):
|
||||
mock_unmap.side_effect = [exception.Invalid, 1]
|
||||
mock_unmap.side_effect = [1]
|
||||
mock_list.side_effect = ['yes']
|
||||
actual = {'driver_volume_type': 'fibre_channel', 'data': {}}
|
||||
self.assertRaises(exception.Invalid,
|
||||
self.driver.terminate_connection, test_volume,
|
||||
connector)
|
||||
ret = self.driver.terminate_connection(test_volume, connector)
|
||||
self.assertEqual(actual, ret)
|
||||
mock_unmap.assert_called_with(test_volume, connector, 'wwpns')
|
||||
|
||||
ret = self.driver.terminate_connection(test_volume, connector)
|
||||
self.assertEqual(actual, ret)
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
# Copyright 2014 Objectif Libre
|
||||
# Copyright 2015 DotHill Systems
|
||||
# Copyright 2015 Dot Hill Systems Corp.
|
||||
# Copyright 2016 Seagate Technology or one of its affiliates
|
||||
#
|
||||
# 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
|
||||
@ -25,28 +26,83 @@ import requests
|
||||
import six
|
||||
|
||||
from cinder import exception
|
||||
from cinder.i18n import _LE
|
||||
from cinder.i18n import _, _LE, _LW, _LI
|
||||
from cinder import utils
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DotHillClient(object):
|
||||
def __init__(self, host, login, password, protocol, ssl_verify):
|
||||
self._mgmt_ip_addrs = list(map(str.strip, host.split(',')))
|
||||
self._login = login
|
||||
self._password = password
|
||||
self._base_url = "%s://%s/api" % (protocol, host)
|
||||
self._protocol = protocol
|
||||
self._session_key = None
|
||||
self.ssl_verify = ssl_verify
|
||||
self._set_host(self._mgmt_ip_addrs[0])
|
||||
self._fw = ''
|
||||
self._luns_in_use_by_host = {}
|
||||
|
||||
def _set_host(self, ip_addr):
|
||||
self._curr_ip_addr = ip_addr
|
||||
self._base_url = "%s://%s/api" % (self._protocol, ip_addr)
|
||||
|
||||
def _get_auth_token(self, xml):
|
||||
"""Parse an XML authentication reply to extract the session key."""
|
||||
self._session_key = None
|
||||
tree = etree.XML(xml)
|
||||
if tree.findtext(".//PROPERTY[@name='response-type']") == "success":
|
||||
self._session_key = tree.findtext(".//PROPERTY[@name='response']")
|
||||
try:
|
||||
tree = etree.XML(xml)
|
||||
if (tree.findtext(".//PROPERTY[@name='response-type']") ==
|
||||
"success"):
|
||||
self._session_key = (
|
||||
tree.findtext(".//PROPERTY[@name='response']"))
|
||||
except Exception as e:
|
||||
msg = _("Cannot parse session key: %s") % e.msg
|
||||
raise exception.DotHillConnectionError(message=msg)
|
||||
|
||||
def login(self):
|
||||
"""Authenticates the service on the device."""
|
||||
if self._session_key is None:
|
||||
return self.session_login()
|
||||
|
||||
def session_login(self):
|
||||
"""Authenticates the service on the device.
|
||||
|
||||
Tries all the IP addrs listed in the san_ip parameter
|
||||
until a working one is found or the list is exhausted.
|
||||
"""
|
||||
|
||||
try:
|
||||
self._get_session_key()
|
||||
self.get_firmware_version()
|
||||
LOG.debug("Logged in to array at %s (session %s)",
|
||||
self._base_url, self._session_key)
|
||||
return
|
||||
except exception.DotHillConnectionError:
|
||||
not_responding = self._curr_ip_addr
|
||||
LOG.exception(_LE('session_login failed to connect to %s'),
|
||||
self._curr_ip_addr)
|
||||
# Loop through the remaining management addresses
|
||||
# to find one that's up.
|
||||
for host in self._mgmt_ip_addrs:
|
||||
if host is not_responding:
|
||||
continue
|
||||
self._set_host(host)
|
||||
try:
|
||||
self._get_session_key()
|
||||
return
|
||||
except exception.DotHillConnectionError:
|
||||
LOG.error(_LE('Failed to connect to %s'),
|
||||
self._curr_ip_addr)
|
||||
continue
|
||||
raise exception.DotHillConnectionError(
|
||||
message=_("Failed to log in to management controller"))
|
||||
|
||||
@utils.synchronized(__name__, external = True)
|
||||
def _get_session_key(self):
|
||||
"""Retrieve a session key from the array."""
|
||||
|
||||
self._session_key = None
|
||||
hash_ = "%s_%s" % (self._login, self._password)
|
||||
if six.PY3:
|
||||
hash_ = hash_.encode('utf-8')
|
||||
@ -55,12 +111,14 @@ class DotHillClient(object):
|
||||
|
||||
url = self._base_url + "/login/" + digest
|
||||
try:
|
||||
xml = requests.get(url, verify=self.ssl_verify)
|
||||
xml = requests.get(url, verify=self.ssl_verify, timeout=30)
|
||||
except requests.exceptions.RequestException:
|
||||
raise exception.DotHillConnectionError
|
||||
msg = _("Failed to obtain MC session key")
|
||||
LOG.exception(msg)
|
||||
raise exception.DotHillConnectionError(message=msg)
|
||||
|
||||
self._get_auth_token(xml.text.encode('utf8'))
|
||||
|
||||
LOG.debug("session key = %s", self._session_key)
|
||||
if self._session_key is None:
|
||||
raise exception.DotHillAuthenticationError
|
||||
|
||||
@ -97,22 +155,64 @@ class DotHillClient(object):
|
||||
return url
|
||||
|
||||
def _request(self, path, *args, **kargs):
|
||||
"""Performs an HTTP request on the device.
|
||||
"""Performs an API request on the array, with retry.
|
||||
|
||||
Propagates a DotHillConnectionError if no valid response is
|
||||
received from the array, e.g. if the network is down.
|
||||
|
||||
Propagates a DotHillRequestError if the device returned a response
|
||||
but the status is not 0. The device error message will be used
|
||||
in the exception message.
|
||||
|
||||
If the status is OK, returns the XML data for further processing.
|
||||
"""
|
||||
tries_left = 2
|
||||
while tries_left > 0:
|
||||
try:
|
||||
return self._api_request(path, *args, **kargs)
|
||||
except exception.DotHillConnectionError as e:
|
||||
if tries_left < 1:
|
||||
LOG.error(_LE("Array Connection error: "
|
||||
"%s (no more retries)"), e.msg)
|
||||
raise
|
||||
# Retry on any network connection errors, SSL errors, etc
|
||||
LOG.error(_LE("Array Connection error: %s (retrying)"), e.msg)
|
||||
except exception.DotHillRequestError as e:
|
||||
if tries_left < 1:
|
||||
LOG.error(_LE("Array Request error: %s (no more retries)"),
|
||||
e.msg)
|
||||
raise
|
||||
# Retry specific errors which may succeed if we log in again
|
||||
# -10027 => The user is not recognized on this system.
|
||||
if '(-10027)' in e.msg:
|
||||
LOG.error(_LE("Array Request error: %s (retrying)"), e.msg)
|
||||
else:
|
||||
raise
|
||||
|
||||
tries_left -= 1
|
||||
self.session_login()
|
||||
|
||||
@utils.synchronized(__name__, external=True)
|
||||
def _api_request(self, path, *args, **kargs):
|
||||
"""Performs an HTTP request on the device, with locking.
|
||||
|
||||
Raises a DotHillRequestError if the device returned but the status is
|
||||
not 0. The device error message will be used in the exception message.
|
||||
|
||||
If the status is OK, returns the XML data for further processing.
|
||||
"""
|
||||
|
||||
url = self._build_request_url(path, *args, **kargs)
|
||||
LOG.debug("DotHill Request URL: %s", url)
|
||||
LOG.debug("Array Request URL: %s (session %s)",
|
||||
url, self._session_key)
|
||||
headers = {'dataType': 'api', 'sessionKey': self._session_key}
|
||||
try:
|
||||
xml = requests.get(url, headers=headers, verify=self.ssl_verify)
|
||||
xml = requests.get(url, headers=headers,
|
||||
verify=self.ssl_verify, timeout=60)
|
||||
tree = etree.XML(xml.text.encode('utf8'))
|
||||
except Exception:
|
||||
raise exception.DotHillConnectionError
|
||||
except Exception as e:
|
||||
message = _("Exception handling URL %(url)s: %(msg)s") % {
|
||||
'url': url, 'msg': e}
|
||||
raise exception.DotHillConnectionError(message=message)
|
||||
|
||||
if path == "/show/volumecopy-status":
|
||||
return tree
|
||||
@ -120,35 +220,77 @@ class DotHillClient(object):
|
||||
return tree
|
||||
|
||||
def logout(self):
|
||||
pass
|
||||
|
||||
def session_logout(self):
|
||||
url = self._base_url + '/exit'
|
||||
try:
|
||||
requests.get(url, verify=self.ssl_verify)
|
||||
requests.get(url, verify=self.ssl_verify, timeout=30)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def is_titanium(self):
|
||||
"""True if array is an older generation."""
|
||||
return True if len(self._fw) > 0 and self._fw[0] == 'T' else False
|
||||
|
||||
def create_volume(self, name, size, backend_name, backend_type):
|
||||
# NOTE: size is in this format: [0-9]+GB
|
||||
# NOTE: size is in this format: [0-9]+GiB
|
||||
path_dict = {'size': size}
|
||||
if backend_type == "linear":
|
||||
path_dict['vdisk'] = backend_name
|
||||
else:
|
||||
path_dict['pool'] = backend_name
|
||||
|
||||
self._request("/create/volume", name, **path_dict)
|
||||
try:
|
||||
self._request("/create/volume", name, **path_dict)
|
||||
except exception.DotHillRequestError as e:
|
||||
# -10186 => The specified name is already in use.
|
||||
# This can occur during controller failover.
|
||||
if '(-10186)' in e.msg:
|
||||
LOG.warning(_LW("Ignoring error in create volume: %s"), e.msg)
|
||||
return None
|
||||
raise
|
||||
|
||||
return None
|
||||
|
||||
def delete_volume(self, name):
|
||||
self._request("/delete/volumes", name)
|
||||
try:
|
||||
self._request("/delete/volumes", name)
|
||||
except exception.DotHillRequestError as e:
|
||||
# -10075 => The specified volume was not found.
|
||||
# This can occur during controller failover.
|
||||
if '(-10075)' in e.msg:
|
||||
LOG.warning(_LW("Ignorning error while deleting %(volume)s:"
|
||||
" %(reason)s"),
|
||||
{'volume': name, 'reason': e.msg})
|
||||
return
|
||||
raise
|
||||
|
||||
def extend_volume(self, name, added_size):
|
||||
self._request("/expand/volume", name, size=added_size)
|
||||
|
||||
def create_snapshot(self, volume_name, snap_name):
|
||||
self._request("/create/snapshots", snap_name, volumes=volume_name)
|
||||
try:
|
||||
self._request("/create/snapshots", snap_name, volumes=volume_name)
|
||||
except exception.DotHillRequestError as e:
|
||||
# -10186 => The specified name is already in use.
|
||||
# This can occur during controller failover.
|
||||
if '(-10186)' in e.msg:
|
||||
LOG.warning(_LW("Ignoring error attempting to create snapshot:"
|
||||
" %s"), e.msg)
|
||||
return None
|
||||
|
||||
def delete_snapshot(self, snap_name):
|
||||
self._request("/delete/snapshot", "cleanup", snap_name)
|
||||
try:
|
||||
self._request("/delete/snapshot", "cleanup", snap_name)
|
||||
except exception.DotHillRequestError as e:
|
||||
# -10050 => The volume was not found on this system.
|
||||
# This can occur during controller failover.
|
||||
if '(-10050)' in e.msg:
|
||||
LOG.warning(_LW("Ignoring unmap error -10050: %s"), e.msg)
|
||||
return None
|
||||
raise
|
||||
|
||||
def backend_exists(self, backend_name, backend_type):
|
||||
try:
|
||||
@ -191,13 +333,40 @@ class DotHillClient(object):
|
||||
"//PROPERTY[@name='lun']")]
|
||||
|
||||
def _get_first_available_lun_for_host(self, host):
|
||||
"""Find next available LUN number.
|
||||
|
||||
Returns a lun number greater than 0 which is not known to be in
|
||||
use between the array and the specified host.
|
||||
"""
|
||||
luns = self.list_luns_for_host(host)
|
||||
self._luns_in_use_by_host[host] = luns
|
||||
lun = 1
|
||||
while True:
|
||||
if lun not in luns:
|
||||
return lun
|
||||
lun += 1
|
||||
|
||||
def _get_next_available_lun_for_host(self, host, after=0):
|
||||
# host can be a comma-separated list of WWPNs; we only use the first.
|
||||
firsthost = host.split(',')[0]
|
||||
LOG.debug('get_next_available_lun: host=%s, firsthost=%s, after=%d',
|
||||
host, firsthost, after)
|
||||
if after == 0:
|
||||
return self._get_first_available_lun_for_host(firsthost)
|
||||
luns = self._luns_in_use_by_host[firsthost]
|
||||
lun = after + 1
|
||||
while lun < 1024:
|
||||
LOG.debug('get_next_available_lun: host=%s, trying lun %d',
|
||||
firsthost, lun)
|
||||
if lun not in luns:
|
||||
LOG.debug('get_next_available_lun: host=%s, RETURNING lun %d',
|
||||
firsthost, lun)
|
||||
return lun
|
||||
lun += 1
|
||||
raise exception.DotHillRequestError(
|
||||
message=_("No LUNs available for mapping to host %s.") % host)
|
||||
|
||||
@utils.synchronized(__name__ + '.map_volume', external=True)
|
||||
def map_volume(self, volume_name, connector, connector_element):
|
||||
if connector_element == 'wwpns':
|
||||
lun = self._get_first_available_lun_for_host(connector['wwpns'][0])
|
||||
@ -207,22 +376,63 @@ class DotHillClient(object):
|
||||
host_status = self._check_host(host)
|
||||
if host_status != 0:
|
||||
hostname = self._safe_hostname(connector['host'])
|
||||
self._request("/create/host", hostname, id=host)
|
||||
try:
|
||||
self._request("/create/host", hostname, id=host)
|
||||
except exception.DotHillRequestError as e:
|
||||
# -10058: The host identifier or nickname is already in use
|
||||
if '(-10058)' in e.msg:
|
||||
LOG.error(_LE("While trying to create host nickname"
|
||||
" %(nickname)s: %(error_msg)s"),
|
||||
{'nickname': hostname,
|
||||
'error_msg': e.msg})
|
||||
else:
|
||||
raise
|
||||
lun = self._get_first_available_lun_for_host(host)
|
||||
|
||||
self._request("/map/volume",
|
||||
volume_name,
|
||||
lun=str(lun),
|
||||
host=host,
|
||||
access="rw")
|
||||
return lun
|
||||
while lun < 255:
|
||||
try:
|
||||
self._request("/map/volume",
|
||||
volume_name,
|
||||
lun=str(lun),
|
||||
host=host,
|
||||
access="rw")
|
||||
return lun
|
||||
except exception.DotHillRequestError as e:
|
||||
# -3177 => "The specified LUN overlaps a previously defined LUN
|
||||
if '(-3177)' in e.msg:
|
||||
LOG.info(_LI("Unable to map volume"
|
||||
" %(volume_name)s to lun %(lun)d:"
|
||||
" %(reason)s"),
|
||||
{'volume_name': volume_name,
|
||||
'lun': lun, 'reason': e.msg})
|
||||
lun = self._get_next_available_lun_for_host(host,
|
||||
after=lun)
|
||||
continue
|
||||
raise
|
||||
except Exception as e:
|
||||
LOG.error(_LE("Error while mapping volume"
|
||||
" %(volume_name)s to lun %(lun)d:"),
|
||||
{'volume_name': volume_name, 'lun': lun},
|
||||
e)
|
||||
raise
|
||||
|
||||
raise exception.DotHillRequestError(
|
||||
message=_("Failed to find a free LUN for host %s") % host)
|
||||
|
||||
def unmap_volume(self, volume_name, connector, connector_element):
|
||||
if connector_element == 'wwpns':
|
||||
host = ",".join(connector['wwpns'])
|
||||
else:
|
||||
host = connector['initiator']
|
||||
self._request("/unmap/volume", volume_name, host=host)
|
||||
try:
|
||||
self._request("/unmap/volume", volume_name, host=host)
|
||||
except exception.DotHillRequestError as e:
|
||||
# -10050 => The volume was not found on this system.
|
||||
# This can occur during controller failover.
|
||||
if '(-10050)' in e.msg:
|
||||
LOG.warning(_LW("Ignoring unmap error -10050: %s"), e.msg)
|
||||
return None
|
||||
raise
|
||||
|
||||
def get_active_target_ports(self):
|
||||
ports = []
|
||||
@ -332,14 +542,16 @@ class DotHillClient(object):
|
||||
"""Modify an initiator name to match firmware requirements.
|
||||
|
||||
Initiator name cannot include certain characters and cannot exceed
|
||||
15 bytes in 'T' firmware (32 bytes in 'G' firmware).
|
||||
15 bytes in 'T' firmware (31 bytes in 'G' firmware).
|
||||
"""
|
||||
for ch in [',', '"', '\\', '<', '>']:
|
||||
if ch in hostname:
|
||||
hostname = hostname.replace(ch, '')
|
||||
hostname = hostname.replace('.', '_')
|
||||
name_limit = 15 if self.is_titanium() else 31
|
||||
index = len(hostname)
|
||||
if index > 15:
|
||||
index = 15
|
||||
if index > name_limit:
|
||||
index = name_limit
|
||||
return hostname[:index]
|
||||
|
||||
def get_active_iscsi_target_portals(self):
|
||||
@ -393,3 +605,9 @@ class DotHillClient(object):
|
||||
tree = self._request("/show/volumes", volume_name)
|
||||
size = tree.findtext(".//PROPERTY[@name='size-numeric']")
|
||||
return self._get_size(size)
|
||||
|
||||
def get_firmware_version(self):
|
||||
tree = self._request("/show/controllers")
|
||||
self._fw = tree.xpath("//PROPERTY[@name='sc-fw']")[0].text
|
||||
LOG.debug("Array firmware is %s\n", self._fw)
|
||||
return self._fw
|
||||
|
@ -1,5 +1,6 @@
|
||||
# Copyright 2014 Objectif Libre
|
||||
# Copyright 2015 DotHill Systems
|
||||
# Copyright 2015 Dot Hill Systems Corp.
|
||||
# Copyright 2016 Seagate Technology or one of its affiliates
|
||||
#
|
||||
# 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
|
||||
@ -61,7 +62,7 @@ CONF.register_opts(iscsi_opts)
|
||||
|
||||
|
||||
class DotHillCommon(object):
|
||||
VERSION = "1.0"
|
||||
VERSION = "1.6"
|
||||
|
||||
stats = {}
|
||||
|
||||
@ -92,7 +93,6 @@ class DotHillCommon(object):
|
||||
self.client_logout()
|
||||
|
||||
def client_login(self):
|
||||
LOG.debug("Connecting to %s Array.", self.vendor_name)
|
||||
try:
|
||||
self.client.login()
|
||||
except exception.DotHillConnectionError as ex:
|
||||
@ -123,7 +123,6 @@ class DotHillCommon(object):
|
||||
|
||||
def client_logout(self):
|
||||
self.client.logout()
|
||||
LOG.debug("Disconnected from %s Array.", self.vendor_name)
|
||||
|
||||
def _get_vol_name(self, volume_id):
|
||||
volume_name = self._encode_name(volume_id)
|
||||
@ -163,7 +162,7 @@ class DotHillCommon(object):
|
||||
self.client_login()
|
||||
# Use base64 to encode the volume name (UUID is too long for DotHill)
|
||||
volume_name = self._get_vol_name(volume['id'])
|
||||
volume_size = "%dGB" % volume['size']
|
||||
volume_size = "%dGiB" % volume['size']
|
||||
LOG.debug("Create Volume having display_name: %(display_name)s "
|
||||
"name: %(name)s id: %(id)s size: %(size)s",
|
||||
{'display_name': volume['display_name'],
|
||||
@ -229,6 +228,9 @@ class DotHillCommon(object):
|
||||
finally:
|
||||
self.client_logout()
|
||||
|
||||
if volume['size'] > src_vref['size']:
|
||||
self.extend_volume(volume, volume['size'])
|
||||
|
||||
def create_volume_from_snapshot(self, volume, snapshot):
|
||||
self.get_volume_stats(True)
|
||||
self._assert_enough_space_for_copy(volume['size'])
|
||||
@ -249,6 +251,9 @@ class DotHillCommon(object):
|
||||
finally:
|
||||
self.client_logout()
|
||||
|
||||
if volume['size'] > snapshot['volume_size']:
|
||||
self.extend_volume(volume, volume['size'])
|
||||
|
||||
def delete_volume(self, volume):
|
||||
LOG.debug("Deleting Volume: %s", volume['id'])
|
||||
if volume['name_id']:
|
||||
@ -408,17 +413,19 @@ class DotHillCommon(object):
|
||||
volume_name = self._get_vol_name(volume['name_id'])
|
||||
else:
|
||||
volume_name = self._get_vol_name(volume['id'])
|
||||
old_size = volume['size']
|
||||
old_size = self.client.get_volume_size(volume_name)
|
||||
growth_size = int(new_size) - old_size
|
||||
LOG.debug("Extending Volume %(volume_name)s from %(old_size)s to "
|
||||
"%(new_size)s, by %(growth_size)s GB.",
|
||||
"%(new_size)s, by %(growth_size)s GiB.",
|
||||
{'volume_name': volume_name,
|
||||
'old_size': old_size,
|
||||
'new_size': new_size,
|
||||
'growth_size': growth_size, })
|
||||
if growth_size < 1:
|
||||
return
|
||||
self.client_login()
|
||||
try:
|
||||
self.client.extend_volume(volume_name, "%dGB" % growth_size)
|
||||
self.client.extend_volume(volume_name, "%dGiB" % growth_size)
|
||||
except exception.DotHillRequestError as ex:
|
||||
LOG.exception(_LE("Extension of volume %s failed."), volume['id'])
|
||||
raise exception.Invalid(ex)
|
||||
|
@ -1,5 +1,6 @@
|
||||
# Copyright 2014 Objectif Libre
|
||||
# Copyright 2015 DotHill Systems
|
||||
# Copyright 2015 Dot Hill Systems Corp.
|
||||
# Copyright 2016 Seagate Technology or one of its affiliates
|
||||
#
|
||||
# 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
|
||||
@ -36,10 +37,11 @@ class DotHillFCDriver(cinder.volume.driver.FibreChannelDriver):
|
||||
- added support for manage/unmanage volume
|
||||
- added initiator target mapping in FC zoning
|
||||
- added https support
|
||||
|
||||
1.6 - Add management path redundancy and reduce load placed
|
||||
on management controller.
|
||||
"""
|
||||
|
||||
VERSION = "1.0"
|
||||
VERSION = "1.6"
|
||||
|
||||
# ThirdPartySystems CI wiki
|
||||
CI_WIKI_NAME = "Vedams_DotHillDriver_CI"
|
||||
@ -99,13 +101,16 @@ class DotHillFCDriver(cinder.volume.driver.FibreChannelDriver):
|
||||
|
||||
@fczm_utils.RemoveFCZone
|
||||
def terminate_connection(self, volume, connector, **kwargs):
|
||||
self.common.unmap_volume(volume, connector, 'wwpns')
|
||||
info = {'driver_volume_type': 'fibre_channel', 'data': {}}
|
||||
if not self.common.client.list_luns_for_host(connector['wwpns'][0]):
|
||||
ports, init_targ_map = self.get_init_targ_map(connector)
|
||||
info['data'] = {'target_wwn': ports,
|
||||
'initiator_target_map': init_targ_map}
|
||||
return info
|
||||
try:
|
||||
self.common.unmap_volume(volume, connector, 'wwpns')
|
||||
if not self.common.client.list_luns_for_host(
|
||||
connector['wwpns'][0]):
|
||||
ports, init_targ_map = self.get_init_targ_map(connector)
|
||||
info['data'] = {'target_wwn': ports,
|
||||
'initiator_target_map': init_targ_map}
|
||||
finally:
|
||||
return info
|
||||
|
||||
def get_init_targ_map(self, connector):
|
||||
init_targ_map = {}
|
||||
@ -142,7 +147,7 @@ class DotHillFCDriver(cinder.volume.driver.FibreChannelDriver):
|
||||
self.__class__.__name__)
|
||||
return stats
|
||||
|
||||
def create_export(self, context, volume, connector):
|
||||
def create_export(self, context, volume, connector=None):
|
||||
pass
|
||||
|
||||
def ensure_export(self, context, volume):
|
||||
|
@ -1,5 +1,6 @@
|
||||
# Copyright 2014 Objectif Libre
|
||||
# Copyright 2015 DotHill Systems
|
||||
# Copyright 2015 Dot Hill Systems Corp.
|
||||
# Copyright 2016 Seagate Technology or one of its affiliates
|
||||
#
|
||||
# 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
|
||||
@ -45,10 +46,11 @@ class DotHillISCSIDriver(cinder.volume.driver.ISCSIDriver):
|
||||
- added support for retype volume
|
||||
- added support for manage/unmanage volume
|
||||
- added https support
|
||||
|
||||
1.6 - Add management path redundancy and reduce load placed
|
||||
on management controller.
|
||||
"""
|
||||
|
||||
VERSION = "1.0"
|
||||
VERSION = "1.6"
|
||||
|
||||
# ThirdPartySystems CI wiki
|
||||
CI_WIKI_NAME = "Vedams_DotHillDriver_CI"
|
||||
@ -158,7 +160,7 @@ class DotHillISCSIDriver(cinder.volume.driver.ISCSIDriver):
|
||||
self.__class__.__name__)
|
||||
return stats
|
||||
|
||||
def create_export(self, context, volume, connector):
|
||||
def create_export(self, context, volume, connector=None):
|
||||
pass
|
||||
|
||||
def ensure_export(self, context, volume):
|
||||
|
@ -50,7 +50,7 @@ CONF.register_opts(iscsi_opts)
|
||||
|
||||
|
||||
class LenovoCommon(dothill_common.DotHillCommon):
|
||||
VERSION = "1.0"
|
||||
VERSION = "1.6"
|
||||
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
|
@ -1,5 +1,6 @@
|
||||
# Copyright 2014 Objectif Libre
|
||||
# Copyright 2015 DotHill Systems
|
||||
# Copyright 2015 Dot Hill Systems Corp.
|
||||
# Copyright 2016 Seagate Technology or one of its affiliates
|
||||
#
|
||||
# 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
|
||||
@ -25,10 +26,11 @@ class LenovoFCDriver(dothill_fc.DotHillFCDriver):
|
||||
|
||||
Version history:
|
||||
1.0 - Inheriting from DotHill cinder drivers.
|
||||
|
||||
1.6 - Add management path redundancy and reduce load placed
|
||||
on management controller.
|
||||
"""
|
||||
|
||||
VERSION = "1.0"
|
||||
VERSION = "1.6"
|
||||
|
||||
# ThirdPartySystems wiki page
|
||||
CI_WIKI_NAME = "Vedams-LenovoStorage_FCISCSI_CI"
|
||||
|
@ -1,5 +1,6 @@
|
||||
# Copyright 2014 Objectif Libre
|
||||
# Copyright 2015 DotHill Systems
|
||||
# Copyright 2015 Dot Hill Systems Corp.
|
||||
# Copyright 2016 Seagate Technology or one of its affiliates
|
||||
#
|
||||
# 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
|
||||
@ -25,10 +26,11 @@ class LenovoISCSIDriver(dothill_iscsi.DotHillISCSIDriver):
|
||||
|
||||
Version history:
|
||||
1.0 - Inheriting from DotHill cinder drivers.
|
||||
|
||||
1.6 - Add management path redundancy and reduce load placed
|
||||
on management controller.
|
||||
"""
|
||||
|
||||
VERSION = "1.0"
|
||||
VERSION = "1.6"
|
||||
|
||||
# ThirdPartySystems wiki page
|
||||
CI_WIKI_NAME = "Vedams-LenovoStorage_FCISCSI_CI"
|
||||
|
@ -51,7 +51,7 @@ CONF.register_opts(iscsi_opts)
|
||||
|
||||
|
||||
class HPMSACommon(dothill_common.DotHillCommon):
|
||||
VERSION = "1.0"
|
||||
VERSION = "1.6"
|
||||
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
|
@ -1,5 +1,6 @@
|
||||
# Copyright 2014 Objectif Libre
|
||||
# Copyright 2015 DotHill Systems
|
||||
# Copyright 2015 Dot Hill Systems Corp.
|
||||
# Copyright 2016 Seagate Technology or one of its affiliates
|
||||
#
|
||||
# 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
|
||||
@ -25,10 +26,11 @@ class HPMSAFCDriver(dothill_fc.DotHillFCDriver):
|
||||
|
||||
Version history:
|
||||
1.0 - Inheriting from DotHill cinder drivers.
|
||||
|
||||
1.6 - Add management path redundancy and reduce load placed
|
||||
on management controller.
|
||||
"""
|
||||
|
||||
VERSION = "1.0"
|
||||
VERSION = "1.6"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(HPMSAFCDriver, self).__init__(*args, **kwargs)
|
||||
|
@ -1,5 +1,6 @@
|
||||
# Copyright 2014 Objectif Libre
|
||||
# Copyright 2015 DotHill Systems
|
||||
# Copyright 2015 Dot Hill Systems Corp.
|
||||
# Copyright 2016 Seagate Technology or one of its affiliates
|
||||
#
|
||||
# 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
|
||||
@ -25,10 +26,11 @@ class HPMSAISCSIDriver(dothill_iscsi.DotHillISCSIDriver):
|
||||
|
||||
Version history:
|
||||
1.0 - Inheriting from DotHill cinder drivers.
|
||||
|
||||
1.6 - Add management path redundancy and reduce load placed
|
||||
on management controller.
|
||||
"""
|
||||
|
||||
VERSION = "1.0"
|
||||
VERSION = "1.6"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(HPMSAISCSIDriver, self).__init__(*args, **kwargs)
|
||||
|
Loading…
Reference in New Issue
Block a user