Merge "Merge changes from kilo version of the dothill driver"

This commit is contained in:
Jenkins 2017-01-27 07:04:20 +00:00 committed by Gerrit Code Review
commit 96be6daf76
11 changed files with 374 additions and 89 deletions

View File

@ -1,5 +1,6 @@
# Copyright 2014 Objectif Libre # Copyright 2014 Objectif Libre
# Copyright 2015 DotHill Systems # 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 # 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 # not use this file except in compliance with the License. You may obtain
@ -35,6 +36,10 @@ resp_login = '''<RESPONSE><OBJECT basetype="status" name="status" oid="1">
<PROPERTY name="response-type-numeric">0</PROPERTY> <PROPERTY name="response-type-numeric">0</PROPERTY>
<PROPERTY name="response">12a1626754554a21d85040760c81b</PROPERTY> <PROPERTY name="response">12a1626754554a21d85040760c81b</PROPERTY>
<PROPERTY name="return-code">1</PROPERTY></OBJECT></RESPONSE>''' <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"> resp_badlogin = '''<RESPONSE><OBJECT basetype="status" name="status" oid="1">
<PROPERTY name="response-type">error</PROPERTY> <PROPERTY name="response-type">error</PROPERTY>
<PROPERTY name="response-type-numeric">1</PROPERTY> <PROPERTY name="response-type-numeric">1</PROPERTY>
@ -109,13 +114,18 @@ test_host = {'capabilities': {'location_info':
'DotHillVolumeDriver:xxxxx:dg02:A'}} 'DotHillVolumeDriver:xxxxx:dg02:A'}}
test_snap = {'id': 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', test_snap = {'id': 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
'volume': {'name_id': None}, 'volume': {'name_id': None},
'volume_id': vol_id, 'volume_id': vol_id, 'display_name': 'test volume',
'display_name': 'test volume', 'name': 'volume', 'size': 10} 'name': 'volume', 'volume_size': 10}
encoded_volid = 'v_O7DDpi8TOWF_9cwnMF' encoded_volid = 'v_O7DDpi8TOWF_9cwnMF'
encoded_snapid = 's_O7DDpi8TOWF_9cwnMF' encoded_snapid = 's_O7DDpi8TOWF_9cwnMF'
dest_volume = {'id': 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', dest_volume = {'id': 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
'source_volid': vol_id, 'source_volid': vol_id,
'display_name': 'test volume', 'name': 'volume', 'size': 10} '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, attached_volume = {'id': vol_id,
'display_name': 'test volume', 'name': 'volume', 'display_name': 'test volume', 'name': 'volume',
'size': 10, 'status': 'in-use', 'size': 10, 'status': 'in-use',
@ -155,14 +165,16 @@ class TestDotHillClient(test.TestCase):
@mock.patch('requests.get') @mock.patch('requests.get')
def test_login(self, mock_requests_get): def test_login(self, mock_requests_get):
m = mock.Mock() m = mock.Mock()
m.text.encode.side_effect = [resp_login]
mock_requests_get.return_value = m mock_requests_get.return_value = m
self.client.login()
self.assertEqual(session_key, self.client._session_key) m.text.encode.side_effect = [resp_badlogin, resp_badlogin]
m.text.encode.side_effect = [resp_badlogin]
self.assertRaises(exception.DotHillAuthenticationError, self.assertRaises(exception.DotHillAuthenticationError,
self.client.login) 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): def test_build_request_url(self):
url = self.client._build_request_url('/path') url = self.client._build_request_url('/path')
self.assertEqual('http://10.0.0.1/api/path', url) self.assertEqual('http://10.0.0.1/api/path', url)
@ -187,13 +199,13 @@ class TestDotHillClient(test.TestCase):
requests.exceptions. requests.exceptions.
RequestException("error")] RequestException("error")]
mock_requests_get.return_value = m mock_requests_get.return_value = m
ret = self.client._request('/path') ret = self.client._api_request('/path')
self.assertTrue(type(ret) == etree._Element) self.assertTrue(type(ret) == etree._Element)
self.assertRaises(exception.DotHillConnectionError, self.assertRaises(exception.DotHillConnectionError,
self.client._request, self.client._api_request,
'/path') '/path')
self.assertRaises(exception.DotHillConnectionError, self.assertRaises(exception.DotHillConnectionError,
self.client._request, self.client._api_request,
'/path') '/path')
def test_assert_response_ok(self): def test_assert_response_ok(self):
@ -379,7 +391,7 @@ class TestFCDotHillCommon(test.TestCase):
ret = self.common.create_volume(test_volume) ret = self.common.create_volume(test_volume)
self.assertIsNone(ret) self.assertIsNone(ret)
mock_create.assert_called_with(encoded_volid, mock_create.assert_called_with(encoded_volid,
"%sGB" % test_volume['size'], "%sGiB" % test_volume['size'],
self.common.backend_name, self.common.backend_name,
self.common.backend_type) self.common.backend_type)
@ -421,7 +433,38 @@ class TestFCDotHillCommon(test.TestCase):
@mock.patch.object(dothill.DotHillClient, 'copy_volume') @mock.patch.object(dothill.DotHillClient, 'copy_volume')
@mock.patch.object(dothill.DotHillClient, 'backend_stats') @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, mock_stats.side_effect = [stats_low_space, stats_large_space,
stats_large_space] stats_large_space]
@ -430,26 +473,30 @@ class TestFCDotHillCommon(test.TestCase):
dest_volume, test_snap) dest_volume, test_snap)
mock_copy.side_effect = [exception.DotHillRequestError, None] mock_copy.side_effect = [exception.DotHillRequestError, None]
mock_get_size.return_value = test_snap['volume_size']
self.assertRaises(exception.Invalid, self.assertRaises(exception.Invalid,
self.common.create_volume_from_snapshot, self.common.create_volume_from_snapshot,
dest_volume, test_snap) 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) self.assertIsNone(ret)
mock_copy.assert_called_with('sqqqqqqqqqqqqqqqqqqq', mock_copy.assert_called_with('sqqqqqqqqqqqqqqqqqqq',
'vqqqqqqqqqqqqqqqqqqq', 'vqqqqqqqqqqqqqqqqqqq',
self.common.backend_name, self.common.backend_name,
self.common.backend_type) 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') @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_extend.side_effect = [exception.DotHillRequestError, None]
mock_size.side_effect = [10, 10]
self.assertRaises(exception.Invalid, self.common.extend_volume, self.assertRaises(exception.Invalid, self.common.extend_volume,
test_volume, 20) test_volume, 20)
ret = self.common.extend_volume(test_volume, 20) ret = self.common.extend_volume(test_volume, 20)
self.assertIsNone(ret) 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') @mock.patch.object(dothill.DotHillClient, 'create_snapshot')
def test_create_snapshot(self, mock_create): def test_create_snapshot(self, mock_create):
@ -622,14 +669,12 @@ class TestDotHillFC(test.TestCase):
@mock.patch.object(dothill_common.DotHillCommon, 'unmap_volume') @mock.patch.object(dothill_common.DotHillCommon, 'unmap_volume')
@mock.patch.object(dothill.DotHillClient, 'list_luns_for_host') @mock.patch.object(dothill.DotHillClient, 'list_luns_for_host')
def test_terminate_connection(self, mock_list, mock_unmap): 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'] mock_list.side_effect = ['yes']
actual = {'driver_volume_type': 'fibre_channel', 'data': {}} actual = {'driver_volume_type': 'fibre_channel', 'data': {}}
self.assertRaises(exception.Invalid, ret = self.driver.terminate_connection(test_volume, connector)
self.driver.terminate_connection, test_volume, self.assertEqual(actual, ret)
connector)
mock_unmap.assert_called_with(test_volume, connector, 'wwpns') mock_unmap.assert_called_with(test_volume, connector, 'wwpns')
ret = self.driver.terminate_connection(test_volume, connector) ret = self.driver.terminate_connection(test_volume, connector)
self.assertEqual(actual, ret) self.assertEqual(actual, ret)

View File

@ -1,5 +1,6 @@
# Copyright 2014 Objectif Libre # 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 # 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 # not use this file except in compliance with the License. You may obtain
@ -25,28 +26,83 @@ import requests
import six import six
from cinder import exception from cinder import exception
from cinder.i18n import _LE from cinder.i18n import _, _LE, _LW, _LI
from cinder import utils
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
class DotHillClient(object): class DotHillClient(object):
def __init__(self, host, login, password, protocol, ssl_verify): def __init__(self, host, login, password, protocol, ssl_verify):
self._mgmt_ip_addrs = list(map(str.strip, host.split(',')))
self._login = login self._login = login
self._password = password self._password = password
self._base_url = "%s://%s/api" % (protocol, host) self._protocol = protocol
self._session_key = None self._session_key = None
self.ssl_verify = ssl_verify 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): def _get_auth_token(self, xml):
"""Parse an XML authentication reply to extract the session key.""" """Parse an XML authentication reply to extract the session key."""
self._session_key = None self._session_key = None
tree = etree.XML(xml) try:
if tree.findtext(".//PROPERTY[@name='response-type']") == "success": tree = etree.XML(xml)
self._session_key = tree.findtext(".//PROPERTY[@name='response']") 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): 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) hash_ = "%s_%s" % (self._login, self._password)
if six.PY3: if six.PY3:
hash_ = hash_.encode('utf-8') hash_ = hash_.encode('utf-8')
@ -55,12 +111,14 @@ class DotHillClient(object):
url = self._base_url + "/login/" + digest url = self._base_url + "/login/" + digest
try: try:
xml = requests.get(url, verify=self.ssl_verify) xml = requests.get(url, verify=self.ssl_verify, timeout=30)
except requests.exceptions.RequestException: 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')) self._get_auth_token(xml.text.encode('utf8'))
LOG.debug("session key = %s", self._session_key)
if self._session_key is None: if self._session_key is None:
raise exception.DotHillAuthenticationError raise exception.DotHillAuthenticationError
@ -97,22 +155,64 @@ class DotHillClient(object):
return url return url
def _request(self, path, *args, **kargs): 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 Raises a DotHillRequestError if the device returned but the status is
not 0. The device error message will be used in the exception message. 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. If the status is OK, returns the XML data for further processing.
""" """
url = self._build_request_url(path, *args, **kargs) 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} headers = {'dataType': 'api', 'sessionKey': self._session_key}
try: 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')) tree = etree.XML(xml.text.encode('utf8'))
except Exception: except Exception as e:
raise exception.DotHillConnectionError message = _("Exception handling URL %(url)s: %(msg)s") % {
'url': url, 'msg': e}
raise exception.DotHillConnectionError(message=message)
if path == "/show/volumecopy-status": if path == "/show/volumecopy-status":
return tree return tree
@ -120,35 +220,77 @@ class DotHillClient(object):
return tree return tree
def logout(self): def logout(self):
pass
def session_logout(self):
url = self._base_url + '/exit' url = self._base_url + '/exit'
try: try:
requests.get(url, verify=self.ssl_verify) requests.get(url, verify=self.ssl_verify, timeout=30)
return True return True
except Exception: except Exception:
return False 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): 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} path_dict = {'size': size}
if backend_type == "linear": if backend_type == "linear":
path_dict['vdisk'] = backend_name path_dict['vdisk'] = backend_name
else: else:
path_dict['pool'] = backend_name 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 return None
def delete_volume(self, name): 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): def extend_volume(self, name, added_size):
self._request("/expand/volume", name, size=added_size) self._request("/expand/volume", name, size=added_size)
def create_snapshot(self, volume_name, snap_name): 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): 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): def backend_exists(self, backend_name, backend_type):
try: try:
@ -191,13 +333,40 @@ class DotHillClient(object):
"//PROPERTY[@name='lun']")] "//PROPERTY[@name='lun']")]
def _get_first_available_lun_for_host(self, host): 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) luns = self.list_luns_for_host(host)
self._luns_in_use_by_host[host] = luns
lun = 1 lun = 1
while True: while True:
if lun not in luns: if lun not in luns:
return lun return lun
lun += 1 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): def map_volume(self, volume_name, connector, connector_element):
if connector_element == 'wwpns': if connector_element == 'wwpns':
lun = self._get_first_available_lun_for_host(connector['wwpns'][0]) lun = self._get_first_available_lun_for_host(connector['wwpns'][0])
@ -207,22 +376,63 @@ class DotHillClient(object):
host_status = self._check_host(host) host_status = self._check_host(host)
if host_status != 0: if host_status != 0:
hostname = self._safe_hostname(connector['host']) 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) lun = self._get_first_available_lun_for_host(host)
self._request("/map/volume", while lun < 255:
volume_name, try:
lun=str(lun), self._request("/map/volume",
host=host, volume_name,
access="rw") lun=str(lun),
return 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): def unmap_volume(self, volume_name, connector, connector_element):
if connector_element == 'wwpns': if connector_element == 'wwpns':
host = ",".join(connector['wwpns']) host = ",".join(connector['wwpns'])
else: else:
host = connector['initiator'] 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): def get_active_target_ports(self):
ports = [] ports = []
@ -332,14 +542,16 @@ class DotHillClient(object):
"""Modify an initiator name to match firmware requirements. """Modify an initiator name to match firmware requirements.
Initiator name cannot include certain characters and cannot exceed 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 [',', '"', '\\', '<', '>']: for ch in [',', '"', '\\', '<', '>']:
if ch in hostname: if ch in hostname:
hostname = hostname.replace(ch, '') hostname = hostname.replace(ch, '')
hostname = hostname.replace('.', '_')
name_limit = 15 if self.is_titanium() else 31
index = len(hostname) index = len(hostname)
if index > 15: if index > name_limit:
index = 15 index = name_limit
return hostname[:index] return hostname[:index]
def get_active_iscsi_target_portals(self): def get_active_iscsi_target_portals(self):
@ -393,3 +605,9 @@ class DotHillClient(object):
tree = self._request("/show/volumes", volume_name) tree = self._request("/show/volumes", volume_name)
size = tree.findtext(".//PROPERTY[@name='size-numeric']") size = tree.findtext(".//PROPERTY[@name='size-numeric']")
return self._get_size(size) 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

View File

@ -1,5 +1,6 @@
# Copyright 2014 Objectif Libre # 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 # 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 # not use this file except in compliance with the License. You may obtain
@ -62,7 +63,7 @@ CONF.register_opts(iscsi_opts)
class DotHillCommon(object): class DotHillCommon(object):
VERSION = "1.0" VERSION = "1.6"
stats = {} stats = {}
@ -93,7 +94,6 @@ class DotHillCommon(object):
self.client_logout() self.client_logout()
def client_login(self): def client_login(self):
LOG.debug("Connecting to %s Array.", self.vendor_name)
try: try:
self.client.login() self.client.login()
except exception.DotHillConnectionError as ex: except exception.DotHillConnectionError as ex:
@ -124,7 +124,6 @@ class DotHillCommon(object):
def client_logout(self): def client_logout(self):
self.client.logout() self.client.logout()
LOG.debug("Disconnected from %s Array.", self.vendor_name)
def _get_vol_name(self, volume_id): def _get_vol_name(self, volume_id):
volume_name = self._encode_name(volume_id) volume_name = self._encode_name(volume_id)
@ -164,7 +163,7 @@ class DotHillCommon(object):
self.client_login() self.client_login()
# Use base64 to encode the volume name (UUID is too long for DotHill) # Use base64 to encode the volume name (UUID is too long for DotHill)
volume_name = self._get_vol_name(volume['id']) 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 " LOG.debug("Create Volume having display_name: %(display_name)s "
"name: %(name)s id: %(id)s size: %(size)s", "name: %(name)s id: %(id)s size: %(size)s",
{'display_name': volume['display_name'], {'display_name': volume['display_name'],
@ -230,6 +229,9 @@ class DotHillCommon(object):
finally: finally:
self.client_logout() self.client_logout()
if volume['size'] > src_vref['size']:
self.extend_volume(volume, volume['size'])
def create_volume_from_snapshot(self, volume, snapshot): def create_volume_from_snapshot(self, volume, snapshot):
self.get_volume_stats(True) self.get_volume_stats(True)
self._assert_enough_space_for_copy(volume['size']) self._assert_enough_space_for_copy(volume['size'])
@ -250,6 +252,9 @@ class DotHillCommon(object):
finally: finally:
self.client_logout() self.client_logout()
if volume['size'] > snapshot['volume_size']:
self.extend_volume(volume, volume['size'])
def delete_volume(self, volume): def delete_volume(self, volume):
LOG.debug("Deleting Volume: %s", volume['id']) LOG.debug("Deleting Volume: %s", volume['id'])
if volume['name_id']: if volume['name_id']:
@ -409,17 +414,19 @@ class DotHillCommon(object):
volume_name = self._get_vol_name(volume['name_id']) volume_name = self._get_vol_name(volume['name_id'])
else: else:
volume_name = self._get_vol_name(volume['id']) 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 growth_size = int(new_size) - old_size
LOG.debug("Extending Volume %(volume_name)s from %(old_size)s to " 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, {'volume_name': volume_name,
'old_size': old_size, 'old_size': old_size,
'new_size': new_size, 'new_size': new_size,
'growth_size': growth_size, }) 'growth_size': growth_size, })
if growth_size < 1:
return
self.client_login() self.client_login()
try: try:
self.client.extend_volume(volume_name, "%dGB" % growth_size) self.client.extend_volume(volume_name, "%dGiB" % growth_size)
except exception.DotHillRequestError as ex: except exception.DotHillRequestError as ex:
LOG.exception(_LE("Extension of volume %s failed."), volume['id']) LOG.exception(_LE("Extension of volume %s failed."), volume['id'])
raise exception.Invalid(ex) raise exception.Invalid(ex)

View File

@ -1,5 +1,6 @@
# Copyright 2014 Objectif Libre # 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 # 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 # 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 support for manage/unmanage volume
- added initiator target mapping in FC zoning - added initiator target mapping in FC zoning
- added https support - 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 # ThirdPartySystems CI wiki
CI_WIKI_NAME = "Vedams_DotHillDriver_CI" CI_WIKI_NAME = "Vedams_DotHillDriver_CI"
@ -103,13 +105,16 @@ class DotHillFCDriver(cinder.volume.driver.FibreChannelDriver):
@fczm_utils.remove_fc_zone @fczm_utils.remove_fc_zone
def terminate_connection(self, volume, connector, **kwargs): def terminate_connection(self, volume, connector, **kwargs):
self.common.unmap_volume(volume, connector, 'wwpns')
info = {'driver_volume_type': 'fibre_channel', 'data': {}} info = {'driver_volume_type': 'fibre_channel', 'data': {}}
if not self.common.client.list_luns_for_host(connector['wwpns'][0]): try:
ports, init_targ_map = self.get_init_targ_map(connector) self.common.unmap_volume(volume, connector, 'wwpns')
info['data'] = {'target_wwn': ports, if not self.common.client.list_luns_for_host(
'initiator_target_map': init_targ_map} connector['wwpns'][0]):
return info 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): def get_init_targ_map(self, connector):
init_targ_map = {} init_targ_map = {}
@ -146,7 +151,7 @@ class DotHillFCDriver(cinder.volume.driver.FibreChannelDriver):
self.__class__.__name__) self.__class__.__name__)
return stats return stats
def create_export(self, context, volume, connector): def create_export(self, context, volume, connector=None):
pass pass
def ensure_export(self, context, volume): def ensure_export(self, context, volume):

View File

@ -1,5 +1,6 @@
# Copyright 2014 Objectif Libre # 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 # 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 # 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 retype volume
- added support for manage/unmanage volume - added support for manage/unmanage volume
- added https support - 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 # ThirdPartySystems CI wiki
CI_WIKI_NAME = "Vedams_DotHillDriver_CI" CI_WIKI_NAME = "Vedams_DotHillDriver_CI"
@ -162,7 +164,7 @@ class DotHillISCSIDriver(cinder.volume.driver.ISCSIDriver):
self.__class__.__name__) self.__class__.__name__)
return stats return stats
def create_export(self, context, volume, connector): def create_export(self, context, volume, connector=None):
pass pass
def ensure_export(self, context, volume): def ensure_export(self, context, volume):

View File

@ -50,7 +50,7 @@ CONF.register_opts(iscsi_opts)
class LenovoCommon(dothill_common.DotHillCommon): class LenovoCommon(dothill_common.DotHillCommon):
VERSION = "1.0" VERSION = "1.6"
def __init__(self, config): def __init__(self, config):
self.config = config self.config = config

View File

@ -1,5 +1,6 @@
# Copyright 2014 Objectif Libre # 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 # 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 # not use this file except in compliance with the License. You may obtain
@ -25,10 +26,11 @@ class LenovoFCDriver(dothill_fc.DotHillFCDriver):
Version history: Version history:
1.0 - Inheriting from DotHill cinder drivers. 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 # ThirdPartySystems wiki page
CI_WIKI_NAME = "Vedams-LenovoStorage_FCISCSI_CI" CI_WIKI_NAME = "Vedams-LenovoStorage_FCISCSI_CI"

View File

@ -1,5 +1,6 @@
# Copyright 2014 Objectif Libre # 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 # 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 # not use this file except in compliance with the License. You may obtain
@ -25,10 +26,11 @@ class LenovoISCSIDriver(dothill_iscsi.DotHillISCSIDriver):
Version history: Version history:
1.0 - Inheriting from DotHill cinder drivers. 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 # ThirdPartySystems wiki page
CI_WIKI_NAME = "Vedams-LenovoStorage_FCISCSI_CI" CI_WIKI_NAME = "Vedams-LenovoStorage_FCISCSI_CI"

View File

@ -51,7 +51,7 @@ CONF.register_opts(iscsi_opts)
class HPMSACommon(dothill_common.DotHillCommon): class HPMSACommon(dothill_common.DotHillCommon):
VERSION = "1.0" VERSION = "1.6"
def __init__(self, config): def __init__(self, config):
self.config = config self.config = config

View File

@ -1,5 +1,6 @@
# Copyright 2014 Objectif Libre # 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 # 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 # not use this file except in compliance with the License. You may obtain
@ -25,10 +26,11 @@ class HPMSAFCDriver(dothill_fc.DotHillFCDriver):
Version history: Version history:
1.0 - Inheriting from DotHill cinder drivers. 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): def __init__(self, *args, **kwargs):
super(HPMSAFCDriver, self).__init__(*args, **kwargs) super(HPMSAFCDriver, self).__init__(*args, **kwargs)

View File

@ -1,5 +1,6 @@
# Copyright 2014 Objectif Libre # 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 # 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 # not use this file except in compliance with the License. You may obtain
@ -25,10 +26,11 @@ class HPMSAISCSIDriver(dothill_iscsi.DotHillISCSIDriver):
Version history: Version history:
1.0 - Inheriting from DotHill cinder drivers. 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): def __init__(self, *args, **kwargs):
super(HPMSAISCSIDriver, self).__init__(*args, **kwargs) super(HPMSAISCSIDriver, self).__init__(*args, **kwargs)