Adds FC and ISCSI Cinder drivers for DotHill Storage Arrays
DocImpact Implements: blueprint dothill-fc-cinder-driver Implements: blueprint dothill-iscsi-cinder-driver Co-Authored-By: Gauvain Pocentek<gauvain.pocentek@objectif-libre.com> Co-Authored-By: Walter A. Boring IV<walter.boring@hp.com> Change-Id: Iaf70ade96d4ad4234cc7e88277ed7b52cf458c2a
This commit is contained in:
parent
eebc694df2
commit
7f7f13bcac
@ -912,3 +912,28 @@ class StorPoolConfigurationMissing(CinderException):
|
||||
class StorPoolConfigurationInvalid(CinderException):
|
||||
message = _("Invalid parameter %(param)s in the %(section)s section "
|
||||
"of the /etc/storpool.conf file: %(error)s")
|
||||
|
||||
|
||||
# DOTHILL drivers
|
||||
class DotHillInvalidBackend(CinderException):
|
||||
message = _("Backend doesn't exist (%(backend)s)")
|
||||
|
||||
|
||||
class DotHillConnectionError(CinderException):
|
||||
message = _("%(message)s")
|
||||
|
||||
|
||||
class DotHillAuthenticationError(CinderException):
|
||||
message = _("%(message)s")
|
||||
|
||||
|
||||
class DotHillNotEnoughSpace(CinderException):
|
||||
message = _("Not enough space on backend (%(backend)s)")
|
||||
|
||||
|
||||
class DotHillRequestError(CinderException):
|
||||
message = _("%(message)s")
|
||||
|
||||
|
||||
class DotHillNotTargetPortal(CinderException):
|
||||
message = _("No active iSCSI portals with supplied iSCSI IPs")
|
||||
|
733
cinder/tests/unit/test_dothill.py
Normal file
733
cinder/tests/unit/test_dothill.py
Normal file
@ -0,0 +1,733 @@
|
||||
# Copyright 2014 Objectif Libre
|
||||
# Copyright 2015 DotHill Systems
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
"""Unit tests for OpenStack Cinder DotHill driver."""
|
||||
|
||||
import urllib2
|
||||
|
||||
from lxml import etree
|
||||
import mock
|
||||
|
||||
from cinder import exception
|
||||
from cinder import test
|
||||
from cinder.volume.drivers.dothill import dothill_client as dothill
|
||||
from cinder.volume.drivers.dothill import dothill_common
|
||||
from cinder.volume.drivers.dothill import dothill_fc
|
||||
from cinder.volume.drivers.dothill import dothill_iscsi
|
||||
from cinder.zonemanager import utils as fczm_utils
|
||||
|
||||
session_key = '12a1626754554a21d85040760c81b'
|
||||
resp_login = '''<RESPONSE><OBJECT basetype="status" name="status" oid="1">
|
||||
<PROPERTY name="response-type">success</PROPERTY>
|
||||
<PROPERTY name="response-type-numeric">0</PROPERTY>
|
||||
<PROPERTY name="response">12a1626754554a21d85040760c81b</PROPERTY>
|
||||
<PROPERTY name="return-code">1</PROPERTY></OBJECT></RESPONSE>'''
|
||||
resp_badlogin = '''<RESPONSE><OBJECT basetype="status" name="status" oid="1">
|
||||
<PROPERTY name="response-type">error</PROPERTY>
|
||||
<PROPERTY name="response-type-numeric">1</PROPERTY>
|
||||
<PROPERTY name="response">Authentication failure</PROPERTY>
|
||||
<PROPERTY name="return-code">1</PROPERTY></OBJECT></RESPONSE>'''
|
||||
response_ok = '''<RESPONSE><OBJECT basetype="status" name="status" oid="1">
|
||||
<PROPERTY name="response">some data</PROPERTY>
|
||||
<PROPERTY name="return-code">0</PROPERTY>
|
||||
</OBJECT></RESPONSE>'''
|
||||
response_not_ok = '''<RESPONSE><OBJECT basetype="status" name="status" oid="1">
|
||||
<PROPERTY name="response">Error Message</PROPERTY>
|
||||
<PROPERTY name="return-code">1</PROPERTY>
|
||||
</OBJECT></RESPONSE>'''
|
||||
response_stats_linear = '''<RESPONSE><OBJECT basetype="virtual-disks">
|
||||
<PROPERTY name="size-numeric">3863830528</PROPERTY>
|
||||
<PROPERTY name="freespace-numeric">3863830528</PROPERTY>
|
||||
</OBJECT></RESPONSE>'''
|
||||
response_stats_realstor = '''<RESPONSE><OBJECT basetype="pools">
|
||||
<PROPERTY name="total-size-numeric">3863830528</PROPERTY>
|
||||
<PROPERTY name="total-avail-numeric">3863830528</PROPERTY>
|
||||
</OBJECT></RESPONSE>'''
|
||||
response_no_lun = '''<RESPONSE></RESPONSE>'''
|
||||
response_lun = '''<RESPONSE><OBJECT basetype="host-view-mappings">
|
||||
<PROPERTY name="lun">1</PROPERTY></OBJECT>
|
||||
<OBJECT basetype="host-view-mappings">
|
||||
<PROPERTY name="lun">4</PROPERTY></OBJECT></RESPONSE>'''
|
||||
response_ports = '''<RESPONSE>
|
||||
<OBJECT basetype="port">
|
||||
<PROPERTY name="port-type">FC</PROPERTY>
|
||||
<PROPERTY name="target-id">id1</PROPERTY>
|
||||
<PROPERTY name="status">Disconnected</PROPERTY></OBJECT>
|
||||
<OBJECT basetype="port">
|
||||
<PROPERTY name="port-type">FC</PROPERTY>
|
||||
<PROPERTY name="target-id">id2</PROPERTY>
|
||||
<PROPERTY name="status">Up</PROPERTY></OBJECT>
|
||||
<OBJECT basetype="port">
|
||||
<PROPERTY name="port-type">iSCSI</PROPERTY>
|
||||
<PROPERTY name="target-id">id3</PROPERTY>
|
||||
<PROPERTY name="%(ip)s" >10.0.0.10</PROPERTY>
|
||||
<PROPERTY name="status">Disconnected</PROPERTY></OBJECT>
|
||||
<OBJECT basetype="port">
|
||||
<PROPERTY name="port-type">iSCSI</PROPERTY>
|
||||
<PROPERTY name="target-id">id4</PROPERTY>
|
||||
<PROPERTY name="%(ip)s" >10.0.0.11</PROPERTY>
|
||||
<PROPERTY name="status">Up</PROPERTY></OBJECT>
|
||||
<OBJECT basetype="port">
|
||||
<PROPERTY name="port-type">iSCSI</PROPERTY>
|
||||
<PROPERTY name="target-id">id5</PROPERTY>
|
||||
<PROPERTY name="%(ip)s" >10.0.0.12</PROPERTY>
|
||||
<PROPERTY name="status">Up</PROPERTY></OBJECT>
|
||||
</RESPONSE>'''
|
||||
|
||||
response_ports_linear = response_ports % {'ip': 'primary-ip-address'}
|
||||
response_ports_realstor = response_ports % {'ip': 'ip-address'}
|
||||
|
||||
|
||||
invalid_xml = '''<RESPONSE></RESPONSE>'''
|
||||
malformed_xml = '''<RESPONSE>'''
|
||||
fake_xml = '''<fakexml></fakexml>'''
|
||||
|
||||
stats_low_space = {'free_capacity_gb': 10, 'total_capacity_gb': 100}
|
||||
stats_large_space = {'free_capacity_gb': 90, 'total_capacity_gb': 100}
|
||||
|
||||
vol_id = 'fceec30e-98bc-4ce5-85ff-d7309cc17cc2'
|
||||
test_volume = {'id': vol_id, 'name_id': None,
|
||||
'display_name': 'test volume', 'name': 'volume', 'size': 10}
|
||||
test_retype_volume = {'attach_status': 'available', 'id': vol_id,
|
||||
'name_id': None, 'display_name': 'test volume',
|
||||
'name': 'volume', 'size': 10}
|
||||
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}
|
||||
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}
|
||||
attached_volume = {'id': vol_id,
|
||||
'display_name': 'test volume', 'name': 'volume',
|
||||
'size': 10, 'status': 'in-use',
|
||||
'attach_status': 'attached'}
|
||||
attaching_volume = {'id': vol_id,
|
||||
'display_name': 'test volume', 'name': 'volume',
|
||||
'size': 10, 'status': 'attaching',
|
||||
'attach_status': 'attached'}
|
||||
detached_volume = {'id': vol_id, 'name_id': None,
|
||||
'display_name': 'test volume', 'name': 'volume',
|
||||
'size': 10, 'status': 'available',
|
||||
'attach_status': 'detached'}
|
||||
|
||||
connector = {'ip': '10.0.0.2',
|
||||
'initiator': 'iqn.1993-08.org.debian:01:222',
|
||||
'wwpns': ["111111111111111", "111111111111112"],
|
||||
'wwnns': ["211111111111111", "211111111111112"],
|
||||
'host': 'fakehost'}
|
||||
invalid_connector = {'ip': '10.0.0.2',
|
||||
'initiator': '',
|
||||
'wwpns': [],
|
||||
'wwnns': [],
|
||||
'host': 'fakehost'}
|
||||
|
||||
|
||||
class TestDotHillClient(test.TestCase):
|
||||
def setUp(self):
|
||||
super(TestDotHillClient, self).setUp()
|
||||
self.login = 'manage'
|
||||
self.passwd = '!manage'
|
||||
self.ip = '10.0.0.1'
|
||||
self.protocol = 'http'
|
||||
self.client = dothill.DotHillClient(self.ip, self.login, self.passwd,
|
||||
self.protocol)
|
||||
|
||||
@mock.patch('urllib2.urlopen')
|
||||
def test_login(self, mock_url_open):
|
||||
m = mock.Mock()
|
||||
m.read.side_effect = [resp_login]
|
||||
mock_url_open.return_value = m
|
||||
self.client.login()
|
||||
self.assertEqual(session_key, self.client._session_key)
|
||||
m.read.side_effect = [resp_badlogin]
|
||||
self.assertRaises(exception.DotHillAuthenticationError,
|
||||
self.client.login)
|
||||
|
||||
def test_build_request_url(self):
|
||||
url = self.client._build_request_url('/path')
|
||||
self.assertEqual('http://10.0.0.1/api/path', url)
|
||||
url = self.client._build_request_url('/path', arg1='val1')
|
||||
self.assertEqual('http://10.0.0.1/api/path/arg1/val1', url)
|
||||
url = self.client._build_request_url('/path', arg_1='val1')
|
||||
self.assertEqual('http://10.0.0.1/api/path/arg-1/val1', url)
|
||||
url = self.client._build_request_url('/path', 'arg1')
|
||||
self.assertEqual('http://10.0.0.1/api/path/arg1', url)
|
||||
url = self.client._build_request_url('/path', 'arg1', arg2='val2')
|
||||
self.assertEqual('http://10.0.0.1/api/path/arg2/val2/arg1', url)
|
||||
url = self.client._build_request_url('/path', 'arg1', 'arg3',
|
||||
arg2='val2')
|
||||
self.assertEqual('http://10.0.0.1/api/path/arg2/val2/arg1/arg3', url)
|
||||
|
||||
@mock.patch('urllib2.urlopen')
|
||||
def test_request(self, mock_url_open):
|
||||
self.client._session_key = session_key
|
||||
|
||||
m = mock.Mock()
|
||||
m.read.side_effect = [response_ok, malformed_xml,
|
||||
urllib2.URLError("error")]
|
||||
mock_url_open.return_value = m
|
||||
ret = self.client._request('/path')
|
||||
self.assertTrue(type(ret) == etree._Element)
|
||||
self.assertRaises(exception.DotHillConnectionError,
|
||||
self.client._request,
|
||||
'/path')
|
||||
self.assertRaises(exception.DotHillConnectionError,
|
||||
self.client._request,
|
||||
'/path')
|
||||
|
||||
def test_assert_response_ok(self):
|
||||
ok_tree = etree.XML(response_ok)
|
||||
not_ok_tree = etree.XML(response_not_ok)
|
||||
invalid_tree = etree.XML(invalid_xml)
|
||||
ret = self.client._assert_response_ok(ok_tree)
|
||||
self.assertEqual(None, ret)
|
||||
self.assertRaises(exception.DotHillRequestError,
|
||||
self.client._assert_response_ok,
|
||||
not_ok_tree)
|
||||
self.assertRaises(exception.DotHillRequestError,
|
||||
self.client._assert_response_ok, invalid_tree)
|
||||
|
||||
@mock.patch.object(dothill.DotHillClient, '_request')
|
||||
def test_backend_exists(self, mock_request):
|
||||
mock_request.side_effect = [exception.DotHillRequestError,
|
||||
fake_xml]
|
||||
self.assertEqual(False, self.client.backend_exists('backend_name',
|
||||
'linear'))
|
||||
self.assertEqual(True, self.client.backend_exists('backend_name',
|
||||
'linear'))
|
||||
|
||||
@mock.patch.object(dothill.DotHillClient, '_request')
|
||||
def test_backend_stats(self, mock_request):
|
||||
stats = {'free_capacity_gb': 1979,
|
||||
'total_capacity_gb': 1979}
|
||||
linear = etree.XML(response_stats_linear)
|
||||
realstor = etree.XML(response_stats_realstor)
|
||||
mock_request.side_effect = [linear, realstor]
|
||||
|
||||
self.assertEqual(stats, self.client.backend_stats('OpenStack',
|
||||
'linear'))
|
||||
self.assertEqual(stats, self.client.backend_stats('OpenStack',
|
||||
'realstor'))
|
||||
|
||||
@mock.patch.object(dothill.DotHillClient, '_request')
|
||||
def test_get_lun(self, mock_request):
|
||||
mock_request.side_effect = [etree.XML(response_no_lun),
|
||||
etree.XML(response_lun)]
|
||||
ret = self.client._get_first_available_lun_for_host("fakehost")
|
||||
self.assertEqual(1, ret)
|
||||
ret = self.client._get_first_available_lun_for_host("fakehost")
|
||||
self.assertEqual(2, ret)
|
||||
|
||||
@mock.patch.object(dothill.DotHillClient, '_request')
|
||||
def test_get_ports(self, mock_request):
|
||||
mock_request.side_effect = [etree.XML(response_ports)]
|
||||
ret = self.client.get_active_target_ports()
|
||||
self.assertEqual([{'port-type': 'FC',
|
||||
'target-id': 'id2',
|
||||
'status': 'Up'},
|
||||
{'port-type': 'iSCSI',
|
||||
'target-id': 'id4',
|
||||
'status': 'Up'},
|
||||
{'port-type': 'iSCSI',
|
||||
'target-id': 'id5',
|
||||
'status': 'Up'}], ret)
|
||||
|
||||
@mock.patch.object(dothill.DotHillClient, '_request')
|
||||
def test_get_fc_ports(self, mock_request):
|
||||
mock_request.side_effect = [etree.XML(response_ports)]
|
||||
ret = self.client.get_active_fc_target_ports()
|
||||
self.assertEqual(['id2'], ret)
|
||||
|
||||
@mock.patch.object(dothill.DotHillClient, '_request')
|
||||
def test_get_iscsi_iqns(self, mock_request):
|
||||
mock_request.side_effect = [etree.XML(response_ports)]
|
||||
ret = self.client.get_active_iscsi_target_iqns()
|
||||
self.assertEqual(['id4', 'id5'], ret)
|
||||
|
||||
@mock.patch.object(dothill.DotHillClient, '_request')
|
||||
def test_get_iscsi_portals(self, mock_request):
|
||||
portals = {'10.0.0.12': 'Up', '10.0.0.11': 'Up'}
|
||||
mock_request.side_effect = [etree.XML(response_ports_linear),
|
||||
etree.XML(response_ports_realstor)]
|
||||
ret = self.client.get_active_iscsi_target_portals('linear')
|
||||
self.assertEqual(portals, ret)
|
||||
ret = self.client.get_active_iscsi_target_portals('realstor')
|
||||
self.assertEqual(portals, ret)
|
||||
|
||||
|
||||
class FakeConfiguration1(object):
|
||||
dothill_backend_name = 'OpenStack'
|
||||
dothill_backend_type = 'linear'
|
||||
san_ip = '10.0.0.1'
|
||||
san_login = 'manage'
|
||||
san_password = '!manage'
|
||||
dothill_wbi_protocol = 'http'
|
||||
|
||||
def safe_get(self, key):
|
||||
return 'fakevalue'
|
||||
|
||||
|
||||
class FakeConfiguration2(FakeConfiguration1):
|
||||
dothill_iscsi_ips = ['10.0.0.11']
|
||||
use_chap_auth = None
|
||||
|
||||
|
||||
class TestFCDotHillCommon(test.TestCase):
|
||||
def setUp(self):
|
||||
super(TestFCDotHillCommon, self).setUp()
|
||||
self.config = FakeConfiguration1()
|
||||
self.common = dothill_common.DotHillCommon(self.config)
|
||||
self.common.client_login = mock.MagicMock()
|
||||
self.common.client_logout = mock.MagicMock()
|
||||
self.common.serialNumber = "xxxxx"
|
||||
self.common.owner = "A"
|
||||
self.connector_element = "wwpns"
|
||||
|
||||
@mock.patch.object(dothill.DotHillClient, 'get_serial_number')
|
||||
@mock.patch.object(dothill.DotHillClient, 'get_owner_info')
|
||||
@mock.patch.object(dothill.DotHillClient, 'backend_exists')
|
||||
def test_do_setup(self, mock_backend_exists,
|
||||
mock_owner_info, mock_serial_number):
|
||||
mock_backend_exists.side_effect = [False, True]
|
||||
mock_owner_info.return_value = "A"
|
||||
mock_serial_number.return_value = "xxxxx"
|
||||
self.assertRaises(exception.DotHillInvalidBackend,
|
||||
self.common.do_setup, None)
|
||||
self.assertEqual(None, self.common.do_setup(None))
|
||||
mock_backend_exists.assert_called_with(self.common.backend_name,
|
||||
self.common.backend_type)
|
||||
mock_owner_info.assert_called_with(self.common.backend_name)
|
||||
|
||||
def test_vol_name(self):
|
||||
self.assertEqual(encoded_volid, self.common._get_vol_name(vol_id))
|
||||
self.assertEqual(encoded_snapid, self.common._get_snap_name(vol_id))
|
||||
|
||||
def test_check_flags(self):
|
||||
class FakeOptions(object):
|
||||
def __init__(self, d):
|
||||
for k, v in d.items():
|
||||
self.__dict__[k] = v
|
||||
|
||||
options = FakeOptions({'opt1': 'val1', 'opt2': 'val2'})
|
||||
required_flags = ['opt1', 'opt2']
|
||||
ret = self.common.check_flags(options, required_flags)
|
||||
self.assertEqual(None, ret)
|
||||
|
||||
options = FakeOptions({'opt1': 'val1', 'opt2': 'val2'})
|
||||
required_flags = ['opt1', 'opt2', 'opt3']
|
||||
self.assertRaises(exception.Invalid, self.common.check_flags,
|
||||
options, required_flags)
|
||||
|
||||
def test_assert_connector_ok(self):
|
||||
self.assertRaises(exception.InvalidInput,
|
||||
self.common._assert_connector_ok, invalid_connector,
|
||||
self.connector_element)
|
||||
self.assertIsNone(self.common._assert_connector_ok(
|
||||
connector,
|
||||
self.connector_element))
|
||||
|
||||
@mock.patch.object(dothill.DotHillClient, 'backend_stats')
|
||||
def test_update_volume_stats(self, mock_stats):
|
||||
mock_stats.side_effect = [exception.DotHillRequestError,
|
||||
stats_large_space]
|
||||
|
||||
self.assertRaises(exception.Invalid, self.common._update_volume_stats)
|
||||
mock_stats.assert_called_with(self.common.backend_name,
|
||||
self.common.backend_type)
|
||||
ret = self.common._update_volume_stats()
|
||||
|
||||
self.assertEqual(None, ret)
|
||||
self.assertEqual({'driver_version': self.common.VERSION,
|
||||
'pools': [{'QoS_support': False,
|
||||
'free_capacity_gb': 90,
|
||||
'location_info':
|
||||
'DotHillVolumeDriver:xxxxx:OpenStack:A',
|
||||
'pool_name': 'OpenStack',
|
||||
'total_capacity_gb': 100}],
|
||||
'storage_protocol': None,
|
||||
'vendor_name': 'DotHill',
|
||||
'volume_backend_name': None}, self.common.stats)
|
||||
|
||||
@mock.patch.object(dothill.DotHillClient, 'create_volume')
|
||||
def test_create_volume(self, mock_create):
|
||||
mock_create.side_effect = [exception.DotHillRequestError, None]
|
||||
|
||||
self.assertRaises(exception.Invalid, self.common.create_volume,
|
||||
test_volume)
|
||||
ret = self.common.create_volume(test_volume)
|
||||
self.assertEqual(None, ret)
|
||||
mock_create.assert_called_with(encoded_volid,
|
||||
"%sGB" % test_volume['size'],
|
||||
self.common.backend_name,
|
||||
self.common.backend_type)
|
||||
|
||||
@mock.patch.object(dothill.DotHillClient, 'delete_volume')
|
||||
def test_delete_volume(self, mock_delete):
|
||||
not_found_e = exception.DotHillRequestError(
|
||||
'The volume was not found on this system.')
|
||||
mock_delete.side_effect = [not_found_e, exception.DotHillRequestError,
|
||||
None]
|
||||
self.assertEqual(None, self.common.delete_volume(test_volume))
|
||||
self.assertRaises(exception.Invalid, self.common.delete_volume,
|
||||
test_volume)
|
||||
self.assertEqual(None, self.common.delete_volume(test_volume))
|
||||
mock_delete.assert_called_with(encoded_volid)
|
||||
|
||||
@mock.patch.object(dothill.DotHillClient, 'copy_volume')
|
||||
@mock.patch.object(dothill.DotHillClient, 'backend_stats')
|
||||
def test_create_cloned_volume(self, 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, 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, detached_volume)
|
||||
|
||||
ret = self.common.create_cloned_volume(dest_volume, detached_volume)
|
||||
self.assertEqual(None, ret)
|
||||
|
||||
mock_copy.assert_called_with(encoded_volid,
|
||||
'vqqqqqqqqqqqqqqqqqqq',
|
||||
0, self.common.backend_name)
|
||||
|
||||
@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_stats.side_effect = [stats_low_space, stats_large_space,
|
||||
stats_large_space]
|
||||
|
||||
self.assertRaises(exception.DotHillNotEnoughSpace,
|
||||
self.common.create_volume_from_snapshot,
|
||||
dest_volume, test_snap)
|
||||
|
||||
mock_copy.side_effect = [exception.DotHillRequestError, None]
|
||||
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)
|
||||
self.assertEqual(None, ret)
|
||||
mock_copy.assert_called_with('sqqqqqqqqqqqqqqqqqqq',
|
||||
'vqqqqqqqqqqqqqqqqqqq',
|
||||
0, self.common.backend_name)
|
||||
|
||||
@mock.patch.object(dothill.DotHillClient, 'extend_volume')
|
||||
def test_extend_volume(self, mock_extend):
|
||||
mock_extend.side_effect = [exception.DotHillRequestError, None]
|
||||
|
||||
self.assertRaises(exception.Invalid, self.common.extend_volume,
|
||||
test_volume, 20)
|
||||
ret = self.common.extend_volume(test_volume, 20)
|
||||
self.assertEqual(None, ret)
|
||||
mock_extend.assert_called_with(encoded_volid, '10GB')
|
||||
|
||||
@mock.patch.object(dothill.DotHillClient, 'create_snapshot')
|
||||
def test_create_snapshot(self, mock_create):
|
||||
mock_create.side_effect = [exception.DotHillRequestError, None]
|
||||
|
||||
self.assertRaises(exception.Invalid, self.common.create_snapshot,
|
||||
test_snap)
|
||||
ret = self.common.create_snapshot(test_snap)
|
||||
self.assertEqual(None, ret)
|
||||
mock_create.assert_called_with(encoded_volid, 'sqqqqqqqqqqqqqqqqqqq')
|
||||
|
||||
@mock.patch.object(dothill.DotHillClient, 'delete_snapshot')
|
||||
def test_delete_snapshot(self, mock_delete):
|
||||
not_found_e = exception.DotHillRequestError(
|
||||
'The volume was not found on this system.')
|
||||
mock_delete.side_effect = [not_found_e, exception.DotHillRequestError,
|
||||
None]
|
||||
|
||||
self.assertEqual(None, self.common.delete_snapshot(test_snap))
|
||||
self.assertRaises(exception.Invalid, self.common.delete_snapshot,
|
||||
test_snap)
|
||||
self.assertEqual(None, self.common.delete_snapshot(test_snap))
|
||||
mock_delete.assert_called_with('sqqqqqqqqqqqqqqqqqqq')
|
||||
|
||||
@mock.patch.object(dothill.DotHillClient, 'map_volume')
|
||||
def test_map_volume(self, mock_map):
|
||||
mock_map.side_effect = [exception.DotHillRequestError, 10]
|
||||
|
||||
self.assertRaises(exception.Invalid, self.common.map_volume,
|
||||
test_volume, connector, self.connector_element)
|
||||
lun = self.common.map_volume(test_volume, connector,
|
||||
self.connector_element)
|
||||
self.assertEqual(10, lun)
|
||||
mock_map.assert_called_with(encoded_volid,
|
||||
connector, self.connector_element)
|
||||
|
||||
@mock.patch.object(dothill.DotHillClient, 'unmap_volume')
|
||||
def test_unmap_volume(self, mock_unmap):
|
||||
mock_unmap.side_effect = [exception.DotHillRequestError, None]
|
||||
|
||||
self.assertRaises(exception.Invalid, self.common.unmap_volume,
|
||||
test_volume, connector, self.connector_element)
|
||||
ret = self.common.unmap_volume(test_volume, connector,
|
||||
self.connector_element)
|
||||
self.assertEqual(None, ret)
|
||||
mock_unmap.assert_called_with(encoded_volid, connector,
|
||||
self.connector_element)
|
||||
|
||||
@mock.patch.object(dothill.DotHillClient, 'copy_volume')
|
||||
@mock.patch.object(dothill.DotHillClient, 'delete_volume')
|
||||
@mock.patch.object(dothill.DotHillClient, 'modify_volume_name')
|
||||
def test_retype(self, mock_modify, mock_delete, mock_copy):
|
||||
mock_copy.side_effect = [exception.DotHillRequestError, None]
|
||||
self.assertRaises(exception.Invalid, self.common.migrate_volume,
|
||||
test_retype_volume, test_host)
|
||||
ret = self.common.migrate_volume(test_retype_volume, test_host)
|
||||
self.assertEqual((True, None), ret)
|
||||
ret = self.common.migrate_volume(test_retype_volume,
|
||||
{'capabilities': {}})
|
||||
self.assertEqual((False, None), ret)
|
||||
|
||||
@mock.patch.object(dothill_common.DotHillCommon, '_get_vol_name')
|
||||
@mock.patch.object(dothill.DotHillClient, 'modify_volume_name')
|
||||
def test_manage_existing(self, mock_modify, mock_volume):
|
||||
existing_ref = {'source-name': 'xxxx'}
|
||||
mock_modify.side_effect = [exception.DotHillRequestError, None]
|
||||
self.assertRaises(exception.Invalid, self.common.manage_existing,
|
||||
test_volume, existing_ref)
|
||||
ret = self.common.manage_existing(test_volume, existing_ref)
|
||||
self.assertEqual(None, ret)
|
||||
|
||||
@mock.patch.object(dothill.DotHillClient, 'get_volume_size')
|
||||
def test_manage_existing_get_size(self, mock_volume):
|
||||
existing_ref = {'source-name': 'xxxx'}
|
||||
mock_volume.side_effect = [exception.DotHillRequestError, 1]
|
||||
self.assertRaises(exception.Invalid,
|
||||
self.common.manage_existing_get_size,
|
||||
None, existing_ref)
|
||||
ret = self.common.manage_existing_get_size(None, existing_ref)
|
||||
self.assertEqual(1, ret)
|
||||
|
||||
|
||||
class TestISCSIDotHillCommon(TestFCDotHillCommon):
|
||||
def setUp(self):
|
||||
super(TestISCSIDotHillCommon, self).setUp()
|
||||
self.connector_element = 'initiator'
|
||||
|
||||
|
||||
class TestDotHillFC(test.TestCase):
|
||||
@mock.patch.object(dothill_common.DotHillCommon, 'do_setup')
|
||||
def setUp(self, mock_setup):
|
||||
super(TestDotHillFC, self).setUp()
|
||||
self.vendor_name = 'DotHill'
|
||||
|
||||
mock_setup.return_value = True
|
||||
|
||||
def fake_init(self, *args, **kwargs):
|
||||
super(dothill_fc.DotHillFCDriver, self).__init__()
|
||||
self.common = None
|
||||
self.configuration = FakeConfiguration1()
|
||||
self.lookup_service = fczm_utils.create_lookup_service()
|
||||
|
||||
dothill_fc.DotHillFCDriver.__init__ = fake_init
|
||||
self.driver = dothill_fc.DotHillFCDriver()
|
||||
self.driver.do_setup(None)
|
||||
|
||||
def _test_with_mock(self, mock, method, args, expected=None):
|
||||
func = getattr(self.driver, method)
|
||||
mock.side_effect = [exception.Invalid(), None]
|
||||
self.assertRaises(exception.Invalid, func, *args)
|
||||
self.assertEqual(expected, func(*args))
|
||||
|
||||
@mock.patch.object(dothill_common.DotHillCommon, 'create_volume')
|
||||
def test_create_volume(self, mock_create):
|
||||
self._test_with_mock(mock_create, 'create_volume', [None],
|
||||
{'metadata': None})
|
||||
|
||||
@mock.patch.object(dothill_common.DotHillCommon,
|
||||
'create_cloned_volume')
|
||||
def test_create_cloned_volume(self, mock_create):
|
||||
self._test_with_mock(mock_create, 'create_cloned_volume', [None, None],
|
||||
{'metadata': None})
|
||||
|
||||
@mock.patch.object(dothill_common.DotHillCommon,
|
||||
'create_volume_from_snapshot')
|
||||
def test_create_volume_from_snapshot(self, mock_create):
|
||||
self._test_with_mock(mock_create, 'create_volume_from_snapshot',
|
||||
[None, None], None)
|
||||
|
||||
@mock.patch.object(dothill_common.DotHillCommon, 'delete_volume')
|
||||
def test_delete_volume(self, mock_delete):
|
||||
self._test_with_mock(mock_delete, 'delete_volume', [None])
|
||||
|
||||
@mock.patch.object(dothill_common.DotHillCommon, 'create_snapshot')
|
||||
def test_create_snapshot(self, mock_create):
|
||||
self._test_with_mock(mock_create, 'create_snapshot', [None])
|
||||
|
||||
@mock.patch.object(dothill_common.DotHillCommon, 'delete_snapshot')
|
||||
def test_delete_snapshot(self, mock_delete):
|
||||
self._test_with_mock(mock_delete, 'delete_snapshot', [None])
|
||||
|
||||
@mock.patch.object(dothill_common.DotHillCommon, 'extend_volume')
|
||||
def test_extend_volume(self, mock_extend):
|
||||
self._test_with_mock(mock_extend, 'extend_volume', [None, 10])
|
||||
|
||||
@mock.patch.object(dothill_common.DotHillCommon, 'client_logout')
|
||||
@mock.patch.object(dothill_common.DotHillCommon,
|
||||
'get_active_fc_target_ports')
|
||||
@mock.patch.object(dothill_common.DotHillCommon, 'map_volume')
|
||||
@mock.patch.object(dothill_common.DotHillCommon, 'client_login')
|
||||
def test_initialize_connection(self, mock_login, mock_map, mock_ports,
|
||||
mock_logout):
|
||||
mock_login.return_value = None
|
||||
mock_logout.return_value = None
|
||||
mock_map.side_effect = [exception.Invalid, 1]
|
||||
mock_ports.side_effect = [['id1']]
|
||||
|
||||
self.assertRaises(exception.Invalid,
|
||||
self.driver.initialize_connection, test_volume,
|
||||
connector)
|
||||
mock_map.assert_called_with(test_volume, connector, 'wwpns')
|
||||
|
||||
ret = self.driver.initialize_connection(test_volume, connector)
|
||||
self.assertEqual({'driver_volume_type': 'fibre_channel',
|
||||
'data': {'initiator_target_map': {
|
||||
'111111111111111': ['id1'],
|
||||
'111111111111112': ['id1']},
|
||||
'target_wwn': ['id1'],
|
||||
'target_lun': 1,
|
||||
'target_discovered': True}}, ret)
|
||||
|
||||
@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_list.side_effect = ['yes']
|
||||
actual = {'driver_volume_type': 'fibre_channel', 'data': {}}
|
||||
self.assertRaises(exception.Invalid,
|
||||
self.driver.terminate_connection, test_volume,
|
||||
connector)
|
||||
mock_unmap.assert_called_with(test_volume, connector, 'wwpns')
|
||||
|
||||
ret = self.driver.terminate_connection(test_volume, connector)
|
||||
self.assertEqual(actual, ret)
|
||||
|
||||
@mock.patch.object(dothill_common.DotHillCommon, 'get_volume_stats')
|
||||
def test_get_volume_stats(self, mock_stats):
|
||||
stats = {'storage_protocol': None,
|
||||
'driver_version': self.driver.VERSION,
|
||||
'volume_backend_name': None,
|
||||
'vendor_name': self.vendor_name,
|
||||
'pools': [{'free_capacity_gb': 90,
|
||||
'reserved_percentage': 0,
|
||||
'total_capacity_gb': 100,
|
||||
'QoS_support': False,
|
||||
'location_info': 'xx:xx:xx:xx',
|
||||
'pool_name': 'x'}]}
|
||||
mock_stats.side_effect = [exception.Invalid, stats, stats]
|
||||
|
||||
self.assertRaises(exception.Invalid, self.driver.get_volume_stats,
|
||||
False)
|
||||
ret = self.driver.get_volume_stats(False)
|
||||
self.assertEqual(stats, ret)
|
||||
|
||||
ret = self.driver.get_volume_stats(True)
|
||||
self.assertEqual(stats, ret)
|
||||
mock_stats.assert_called_with(True)
|
||||
|
||||
@mock.patch.object(dothill_common.DotHillCommon, 'retype')
|
||||
def test_retype(self, mock_retype):
|
||||
mock_retype.side_effect = [exception.Invalid, True, False]
|
||||
args = [None, None, None, None, None]
|
||||
self.assertRaises(exception.Invalid, self.driver.retype, *args)
|
||||
self.assertEqual(True, self.driver.retype(*args))
|
||||
self.assertEqual(False, self.driver.retype(*args))
|
||||
|
||||
@mock.patch.object(dothill_common.DotHillCommon, 'manage_existing')
|
||||
def test_manage_existing(self, mock_manage_existing):
|
||||
self._test_with_mock(mock_manage_existing, 'manage_existing',
|
||||
[None, None])
|
||||
|
||||
@mock.patch.object(dothill_common.DotHillCommon,
|
||||
'manage_existing_get_size')
|
||||
def test_manage_size(self, mock_manage_size):
|
||||
mock_manage_size.side_effect = [exception.Invalid, 1]
|
||||
self.assertRaises(exception.Invalid,
|
||||
self.driver.manage_existing_get_size,
|
||||
None, None)
|
||||
self.assertEqual(1, self.driver.manage_existing_get_size(None, None))
|
||||
|
||||
|
||||
class TestDotHillISCSI(TestDotHillFC):
|
||||
@mock.patch.object(dothill_common.DotHillCommon, 'do_setup')
|
||||
def setUp(self, mock_setup):
|
||||
super(TestDotHillISCSI, self).setUp()
|
||||
self.vendor_name = 'DotHill'
|
||||
mock_setup.return_value = True
|
||||
|
||||
def fake_init(self, *args, **kwargs):
|
||||
super(dothill_iscsi.DotHillISCSIDriver, self).__init__()
|
||||
self.common = None
|
||||
self.configuration = FakeConfiguration2()
|
||||
self.iscsi_ips = ['10.0.0.11']
|
||||
|
||||
dothill_iscsi.DotHillISCSIDriver.__init__ = fake_init
|
||||
self.driver = dothill_iscsi.DotHillISCSIDriver()
|
||||
self.driver.do_setup(None)
|
||||
|
||||
@mock.patch.object(dothill_common.DotHillCommon, 'client_logout')
|
||||
@mock.patch.object(dothill_common.DotHillCommon,
|
||||
'get_active_iscsi_target_portals')
|
||||
@mock.patch.object(dothill_common.DotHillCommon,
|
||||
'get_active_iscsi_target_iqns')
|
||||
@mock.patch.object(dothill_common.DotHillCommon, 'map_volume')
|
||||
@mock.patch.object(dothill_common.DotHillCommon, 'client_login')
|
||||
def test_initialize_connection(self, mock_login, mock_map, mock_iqns,
|
||||
mock_portals, mock_logout):
|
||||
mock_login.return_value = None
|
||||
mock_logout.return_value = None
|
||||
mock_map.side_effect = [exception.Invalid, 1]
|
||||
self.driver.iscsi_ips = ['10.0.0.11']
|
||||
self.driver.initialize_iscsi_ports()
|
||||
mock_iqns.side_effect = [['id2']]
|
||||
mock_portals.side_effect = {'10.0.0.11': 'Up', '10.0.0.12': 'Up'}
|
||||
|
||||
self.assertRaises(exception.Invalid,
|
||||
self.driver.initialize_connection, test_volume,
|
||||
connector)
|
||||
mock_map.assert_called_with(test_volume, connector, 'initiator')
|
||||
|
||||
ret = self.driver.initialize_connection(test_volume, connector)
|
||||
self.assertEqual({'driver_volume_type': 'iscsi',
|
||||
'data': {'target_iqn': 'id2',
|
||||
'target_lun': 1,
|
||||
'target_discovered': True,
|
||||
'target_portal': '10.0.0.11:3260'}}, ret)
|
||||
|
||||
@mock.patch.object(dothill_common.DotHillCommon, 'unmap_volume')
|
||||
def test_terminate_connection(self, mock_unmap):
|
||||
mock_unmap.side_effect = [exception.Invalid, 1]
|
||||
|
||||
self.assertRaises(exception.Invalid,
|
||||
self.driver.terminate_connection, test_volume,
|
||||
connector)
|
||||
mock_unmap.assert_called_with(test_volume, connector, 'initiator')
|
||||
|
||||
ret = self.driver.terminate_connection(test_volume, connector)
|
||||
self.assertEqual(None, ret)
|
0
cinder/volume/drivers/dothill/__init__.py
Normal file
0
cinder/volume/drivers/dothill/__init__.py
Normal file
336
cinder/volume/drivers/dothill/dothill_client.py
Normal file
336
cinder/volume/drivers/dothill/dothill_client.py
Normal file
@ -0,0 +1,336 @@
|
||||
# Copyright 2014 Objectif Libre
|
||||
# Copyright 2015 DotHill Systems
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
from hashlib import md5
|
||||
import math
|
||||
import time
|
||||
import urllib2
|
||||
|
||||
from lxml import etree
|
||||
from oslo_log import log as logging
|
||||
|
||||
from cinder import exception
|
||||
from cinder.i18n import _LE
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DotHillClient(object):
|
||||
def __init__(self, host, login, password, protocol):
|
||||
self._login = login
|
||||
self._password = password
|
||||
self._base_url = "%s://%s/api" % (protocol, host)
|
||||
self._session_key = None
|
||||
|
||||
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']")
|
||||
|
||||
def login(self):
|
||||
"""Authenticates the service on the device."""
|
||||
hash_ = md5("%s_%s" % (self._login, self._password))
|
||||
digest = hash_.hexdigest()
|
||||
|
||||
url = self._base_url + "/login/" + digest
|
||||
try:
|
||||
xml = urllib2.urlopen(url).read()
|
||||
except urllib2.URLError:
|
||||
raise exception.DotHillConnectionError
|
||||
|
||||
self._get_auth_token(xml)
|
||||
|
||||
if self._session_key is None:
|
||||
raise exception.DotHillAuthenticationError
|
||||
|
||||
def _assert_response_ok(self, tree):
|
||||
"""Parses the XML returned by the device to check the return code.
|
||||
|
||||
Raises a DotHillRequestError error if the return code is not 0.
|
||||
"""
|
||||
return_code = tree.findtext(".//PROPERTY[@name='return-code']")
|
||||
if return_code and return_code != '0':
|
||||
raise exception.DotHillRequestError(
|
||||
message=tree.findtext(".//PROPERTY[@name='response']"))
|
||||
elif not return_code:
|
||||
raise exception.DotHillRequestError(message="No status found")
|
||||
|
||||
def _build_request_url(self, path, *args, **kargs):
|
||||
url = self._base_url + path
|
||||
if kargs:
|
||||
url += '/' + '/'.join(["%s/%s" % (k.replace('_', '-'), v)
|
||||
for (k, v) in kargs.items()])
|
||||
if args:
|
||||
url += '/' + '/'.join(args)
|
||||
|
||||
return url
|
||||
|
||||
def _request(self, path, *args, **kargs):
|
||||
"""Performs an HTTP request on the device.
|
||||
|
||||
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)
|
||||
headers = {'dataType': 'api', 'sessionKey': self._session_key}
|
||||
req = urllib2.Request(url, headers=headers)
|
||||
try:
|
||||
xml = urllib2.urlopen(req).read()
|
||||
tree = etree.XML(xml)
|
||||
except Exception:
|
||||
raise exception.DotHillConnectionError
|
||||
|
||||
if path == "/show/volumecopy-status":
|
||||
return tree
|
||||
self._assert_response_ok(tree)
|
||||
return tree
|
||||
|
||||
def logout(self):
|
||||
url = self._base_url + '/exit'
|
||||
try:
|
||||
urllib2.urlopen(url)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def create_volume(self, name, size, backend_name, backend_type):
|
||||
# NOTE: size is in this format: [0-9]+GB
|
||||
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)
|
||||
return None
|
||||
|
||||
def delete_volume(self, name):
|
||||
self._request("/delete/volumes", name)
|
||||
|
||||
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)
|
||||
|
||||
def delete_snapshot(self, snap_name):
|
||||
self._request("/delete/snapshot", "cleanup", snap_name)
|
||||
|
||||
def backend_exists(self, backend_name, backend_type):
|
||||
try:
|
||||
if backend_type == "linear":
|
||||
path = "/show/vdisks"
|
||||
else:
|
||||
path = "/show/pools"
|
||||
self._request(path, backend_name)
|
||||
return True
|
||||
except exception.DotHillRequestError:
|
||||
return False
|
||||
|
||||
def _get_size(self, size):
|
||||
return int(math.ceil(float(size) * 512 / (10 ** 9)))
|
||||
|
||||
def backend_stats(self, backend_name, backend_type):
|
||||
stats = {'free_capacity_gb': 0,
|
||||
'total_capacity_gb': 0}
|
||||
prop_list = []
|
||||
if backend_type == "linear":
|
||||
path = "/show/vdisks"
|
||||
prop_list = ["size-numeric", "freespace-numeric"]
|
||||
else:
|
||||
path = "/show/pools"
|
||||
prop_list = ["total-size-numeric", "total-avail-numeric"]
|
||||
tree = self._request(path, backend_name)
|
||||
|
||||
size = tree.findtext(".//PROPERTY[@name='%s']" % prop_list[0])
|
||||
if size:
|
||||
stats['total_capacity_gb'] = self._get_size(size)
|
||||
|
||||
size = tree.findtext(".//PROPERTY[@name='%s']" % prop_list[1])
|
||||
if size:
|
||||
stats['free_capacity_gb'] = self._get_size(size)
|
||||
return stats
|
||||
|
||||
def list_luns_for_host(self, host):
|
||||
tree = self._request("/show/host-maps", host)
|
||||
return [int(prop.text) for prop in tree.xpath(
|
||||
"//PROPERTY[@name='lun']")]
|
||||
|
||||
def _get_first_available_lun_for_host(self, host):
|
||||
luns = self.list_luns_for_host(host)
|
||||
lun = 1
|
||||
while True:
|
||||
if lun not in luns:
|
||||
return lun
|
||||
lun += 1
|
||||
|
||||
def map_volume(self, volume_name, connector, connector_element):
|
||||
if connector_element == 'wwpns':
|
||||
lun = self._get_first_available_lun_for_host(connector['wwpns'][0])
|
||||
host = ",".join(connector['wwpns'])
|
||||
else:
|
||||
host = connector['initiator']
|
||||
host_status = self._check_host(host)
|
||||
if host_status != 0:
|
||||
hostname = self._safe_hostname(connector['host'])
|
||||
self._request("/create/host", hostname, id=host)
|
||||
lun = self._get_first_available_lun_for_host(host)
|
||||
|
||||
self._request("/map/volume",
|
||||
volume_name,
|
||||
lun=str(lun),
|
||||
host=host,
|
||||
access="rw")
|
||||
return lun
|
||||
|
||||
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)
|
||||
|
||||
def get_active_target_ports(self):
|
||||
ports = []
|
||||
tree = self._request("/show/ports")
|
||||
|
||||
for obj in tree.xpath("//OBJECT[@basetype='port']"):
|
||||
port = {prop.get('name'): prop.text
|
||||
for prop in obj.iter("PROPERTY")
|
||||
if prop.get('name') in
|
||||
["port-type", "target-id", "status"]}
|
||||
if port['status'] == 'Up':
|
||||
ports.append(port)
|
||||
return ports
|
||||
|
||||
def get_active_fc_target_ports(self):
|
||||
return [port['target-id'] for port in self.get_active_target_ports()
|
||||
if port['port-type'] == "FC"]
|
||||
|
||||
def get_active_iscsi_target_iqns(self):
|
||||
return [port['target-id'] for port in self.get_active_target_ports()
|
||||
if port['port-type'] == "iSCSI"]
|
||||
|
||||
def copy_volume(self, src_name, dest_name, same_bknd, dest_bknd_name):
|
||||
self._request("/volumecopy",
|
||||
dest_name,
|
||||
dest_vdisk=dest_bknd_name,
|
||||
source_volume=src_name,
|
||||
prompt='yes')
|
||||
|
||||
if same_bknd == 0:
|
||||
return
|
||||
|
||||
count = 0
|
||||
while True:
|
||||
tree = self._request("/show/volumecopy-status")
|
||||
return_code = tree.findtext(".//PROPERTY[@name='return-code']")
|
||||
|
||||
if return_code == '0':
|
||||
status = tree.findtext(".//PROPERTY[@name='progress']")
|
||||
progress = False
|
||||
if status:
|
||||
progress = True
|
||||
LOG.debug("Volume copy is in progress: %s", status)
|
||||
if not progress:
|
||||
LOG.debug("Volume copy completed: %s", status)
|
||||
break
|
||||
else:
|
||||
if count >= 5:
|
||||
LOG.error(_LE('Error in copying volume: %s'), src_name)
|
||||
raise exception.DotHillRequestError
|
||||
break
|
||||
time.sleep(1)
|
||||
count += 1
|
||||
|
||||
time.sleep(5)
|
||||
|
||||
def _check_host(self, host):
|
||||
host_status = -1
|
||||
tree = self._request("/show/hosts")
|
||||
for prop in tree.xpath("//PROPERTY[@name='host-id' and text()='%s']"
|
||||
% host):
|
||||
host_status = 0
|
||||
return host_status
|
||||
|
||||
def _safe_hostname(self, hostname):
|
||||
"""DotHill hostname restrictions.
|
||||
|
||||
A host name cannot include " , \ in linear and " , < > \ in realstor
|
||||
and can have a max of 15 bytes in linear and 32 bytes in realstor.
|
||||
"""
|
||||
for ch in [',', '"', '\\', '<', '>']:
|
||||
if ch in hostname:
|
||||
hostname = hostname.replace(ch, '')
|
||||
index = len(hostname)
|
||||
if index > 15:
|
||||
index = 15
|
||||
return hostname[:index]
|
||||
|
||||
def get_active_iscsi_target_portals(self, backend_type):
|
||||
# This function returns {'ip': status,}
|
||||
portals = {}
|
||||
prop = ""
|
||||
tree = self._request("/show/ports")
|
||||
if backend_type == "linear":
|
||||
prop = "primary-ip-address"
|
||||
else:
|
||||
prop = "ip-address"
|
||||
|
||||
iscsi_ips = [ip.text for ip in tree.xpath(
|
||||
"//PROPERTY[@name='%s']" % prop)]
|
||||
if not iscsi_ips:
|
||||
return portals
|
||||
for index, port_type in enumerate(tree.xpath(
|
||||
"//PROPERTY[@name='port-type' and text()='iSCSI']")):
|
||||
status = port_type.getparent().findtext("PROPERTY[@name='status']")
|
||||
if status == 'Up':
|
||||
portals[iscsi_ips[index]] = status
|
||||
return portals
|
||||
|
||||
def get_chap_record(self, initiator_name):
|
||||
tree = self._request("/show/chap-records")
|
||||
for prop in tree.xpath("//PROPERTY[@name='initiator-name' and "
|
||||
"text()='%s']" % initiator_name):
|
||||
chap_secret = prop.getparent().findtext("PROPERTY[@name='initiator"
|
||||
"-secret']")
|
||||
return chap_secret
|
||||
|
||||
def create_chap_record(self, initiator_name, chap_secret):
|
||||
self._request("/create/chap-record",
|
||||
name=initiator_name,
|
||||
secret=chap_secret)
|
||||
|
||||
def get_serial_number(self):
|
||||
tree = self._request("/show/system")
|
||||
return tree.findtext(".//PROPERTY[@name='midplane-serial-number']")
|
||||
|
||||
def get_owner_info(self, backend_name):
|
||||
tree = self._request("/show/vdisks", backend_name)
|
||||
return tree.findtext(".//PROPERTY[@name='owner']")
|
||||
|
||||
def modify_volume_name(self, old_name, new_name):
|
||||
self._request("/set/volume", old_name, name=new_name)
|
||||
|
||||
def get_volume_size(self, volume_name):
|
||||
tree = self._request("/show/volumes", volume_name)
|
||||
size = tree.findtext(".//PROPERTY[@name='size-numeric']")
|
||||
return self._get_size(size)
|
542
cinder/volume/drivers/dothill/dothill_common.py
Normal file
542
cinder/volume/drivers/dothill/dothill_common.py
Normal file
@ -0,0 +1,542 @@
|
||||
# Copyright 2014 Objectif Libre
|
||||
# Copyright 2015 DotHill Systems
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
"""
|
||||
Volume driver common utilities for DotHill Storage array
|
||||
"""
|
||||
|
||||
import base64
|
||||
import six
|
||||
import uuid
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
|
||||
from cinder import exception
|
||||
from cinder.i18n import _, _LE
|
||||
from cinder.volume.drivers.dothill import dothill_client as dothill
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
common_opt = [
|
||||
cfg.StrOpt('dothill_backend_name',
|
||||
default='OpenStack',
|
||||
help="VDisk or Pool name to use for volume creation."),
|
||||
cfg.StrOpt('dothill_backend_type',
|
||||
choices=['linear', 'realstor'],
|
||||
help="linear (for VDisk) or realstor (for Pool)."),
|
||||
cfg.StrOpt('dothill_wbi_protocol',
|
||||
choices=['http', 'https'],
|
||||
help="DotHill web interface protocol."),
|
||||
]
|
||||
|
||||
iscsi_opt = [
|
||||
cfg.ListOpt('dothill_iscsi_ips',
|
||||
default=[],
|
||||
help="List of comma separated target iSCSI IP addresses."),
|
||||
]
|
||||
|
||||
CONF = cfg.CONF
|
||||
CONF.register_opts(common_opt)
|
||||
CONF.register_opts(iscsi_opt)
|
||||
|
||||
|
||||
class DotHillCommon(object):
|
||||
VERSION = "1.0"
|
||||
|
||||
stats = {}
|
||||
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
self.vendor_name = "DotHill"
|
||||
self.backend_name = self.config.dothill_backend_name
|
||||
self.backend_type = self.config.dothill_backend_type
|
||||
self.client = dothill.DotHillClient(self.config.san_ip,
|
||||
self.config.san_login,
|
||||
self.config.san_password,
|
||||
self.config.dothill_wbi_protocol)
|
||||
|
||||
def get_version(self):
|
||||
return self.VERSION
|
||||
|
||||
def do_setup(self, context):
|
||||
self.client_login()
|
||||
self._validate_backend()
|
||||
if (self.backend_type == "linear" or
|
||||
(self.backend_type == "realstor" and
|
||||
self.backend_name not in ['A', 'B'])):
|
||||
self._get_owner_info(self.backend_name)
|
||||
self._get_serial_number()
|
||||
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:
|
||||
msg = _("Failed to connect to %(vendor_name)s Array %(host)s: "
|
||||
"%(err)s") % {'vendor_name': self.vendor_name,
|
||||
'host': self.config.san_ip,
|
||||
'err': six.text_type(ex)}
|
||||
LOG.error(msg)
|
||||
raise exception.DotHillConnectionError(message=msg)
|
||||
except exception.DotHillAuthenticationError:
|
||||
msg = _("Failed to log on %s Array "
|
||||
"(invalid login?).") % self.vendor_name
|
||||
LOG.error(msg)
|
||||
raise exception.DotHillAuthenticationError(message=msg)
|
||||
|
||||
def _get_serial_number(self):
|
||||
self.serialNumber = self.client.get_serial_number()
|
||||
|
||||
def _get_owner_info(self, backend_name):
|
||||
self.owner = self.client.get_owner_info(backend_name)
|
||||
|
||||
def _validate_backend(self):
|
||||
if not self.client.backend_exists(self.backend_name,
|
||||
self.backend_type):
|
||||
self.client_logout()
|
||||
raise exception.DotHillInvalidBackend(backend=self.backend_name)
|
||||
|
||||
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)
|
||||
return "v%s" % volume_name
|
||||
|
||||
def _get_snap_name(self, snapshot_id):
|
||||
snapshot_name = self._encode_name(snapshot_id)
|
||||
return "s%s" % snapshot_name
|
||||
|
||||
def _encode_name(self, name):
|
||||
"""Get converted DotHill volume name.
|
||||
|
||||
Converts the openstack volume id from
|
||||
fceec30e-98bc-4ce5-85ff-d7309cc17cc2
|
||||
to
|
||||
v_O7DDpi8TOWF_9cwnMF
|
||||
|
||||
We convert the 128(32*4) bits of the uuid into a 24character long
|
||||
base64 encoded string. This still exceeds the limit of 20 characters
|
||||
so we truncate the name later.
|
||||
"""
|
||||
uuid_str = name.replace("-", "")
|
||||
vol_uuid = uuid.UUID('urn:uuid:%s' % uuid_str)
|
||||
vol_encoded = base64.b64encode(vol_uuid.bytes)
|
||||
vol_encoded = vol_encoded.replace('=', '')
|
||||
|
||||
# + is not a valid character for DotHill
|
||||
vol_encoded = vol_encoded.replace('+', '.')
|
||||
# since we use http URLs to send paramters, '/' is not an acceptable
|
||||
# parameter
|
||||
vol_encoded = vol_encoded.replace('/', '_')
|
||||
|
||||
# NOTE:we limit the size to 20 characters since the array
|
||||
# doesn't support more than that for now. Duplicates should happen very
|
||||
# rarely.
|
||||
# We return 19 chars here because the _get_{vol,snap}_name functions
|
||||
# prepend a character
|
||||
return vol_encoded[:19]
|
||||
|
||||
def check_flags(self, options, required_flags):
|
||||
for flag in required_flags:
|
||||
if not getattr(options, flag, None):
|
||||
msg = _('%s configuration option is not set.') % flag
|
||||
LOG.error(msg)
|
||||
raise exception.InvalidInput(reason=msg)
|
||||
|
||||
def create_volume(self, volume):
|
||||
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']
|
||||
LOG.debug("Create Volume having display_name: %(display_name)s "
|
||||
"name: %(name)s id: %(id)s size: %(size)s",
|
||||
{'display_name': volume['display_name'],
|
||||
'name': volume['name'],
|
||||
'id': volume_name,
|
||||
'size': volume_size, })
|
||||
try:
|
||||
metadata = self.client.create_volume(volume_name,
|
||||
volume_size,
|
||||
self.backend_name,
|
||||
self.backend_type)
|
||||
return metadata
|
||||
except exception.DotHillRequestError as ex:
|
||||
LOG.exception(_LE("Creation of volume %s failed."), volume['id'])
|
||||
raise exception.Invalid(ex)
|
||||
|
||||
finally:
|
||||
self.client_logout()
|
||||
|
||||
def _assert_enough_space_for_copy(self, volume_size):
|
||||
"""The DotHill creates a snap pool before trying to copy the volume.
|
||||
The pool is 5.27GB or 20% of the volume size, whichever is larger.
|
||||
|
||||
Verify that we have enough space for the pool and then copy
|
||||
"""
|
||||
pool_size = max(volume_size * 0.2, 5.27)
|
||||
required_size = pool_size + volume_size
|
||||
|
||||
if required_size > self.stats['pools'][0]['free_capacity_gb']:
|
||||
raise exception.DotHillNotEnoughSpace(backend=self.backend_name)
|
||||
|
||||
def _assert_source_detached(self, volume):
|
||||
"""The DotHill requires a volume to be dettached to clone it.
|
||||
|
||||
Make sure that the volume is not in use when trying to copy it.
|
||||
"""
|
||||
if (volume['status'] != "available" or
|
||||
volume['attach_status'] == "attached"):
|
||||
LOG.error(_LE("Volume must be detached for clone operation."))
|
||||
raise exception.VolumeAttached(volume_id=volume['id'])
|
||||
|
||||
def create_cloned_volume(self, volume, src_vref):
|
||||
if self.backend_type == "realstor" and self.backend_name in ["A", "B"]:
|
||||
msg = _("Create volume from volume(clone) does not have support "
|
||||
"for virtual pool A and B.")
|
||||
LOG.error(msg)
|
||||
raise exception.InvalidInput(reason=msg)
|
||||
self.get_volume_stats(True)
|
||||
self._assert_enough_space_for_copy(volume['size'])
|
||||
self._assert_source_detached(src_vref)
|
||||
LOG.debug("Cloning Volume %(source_id)s to (%(dest_id)s)",
|
||||
{'source_id': volume['source_volid'],
|
||||
'dest_id': volume['id'], })
|
||||
|
||||
if src_vref['name_id']:
|
||||
orig_name = self._get_vol_name(src_vref['name_id'])
|
||||
else:
|
||||
orig_name = self._get_vol_name(volume['source_volid'])
|
||||
dest_name = self._get_vol_name(volume['id'])
|
||||
|
||||
self.client_login()
|
||||
try:
|
||||
self.client.copy_volume(orig_name, dest_name, 0, self.backend_name)
|
||||
return None
|
||||
except exception.DotHillRequestError as ex:
|
||||
LOG.exception(_LE("Cloning of volume %s failed."),
|
||||
volume['source_volid'])
|
||||
raise exception.Invalid(ex)
|
||||
finally:
|
||||
self.client_logout()
|
||||
|
||||
def create_volume_from_snapshot(self, volume, snapshot):
|
||||
if self.backend_type == "realstor" and self.backend_name in ["A", "B"]:
|
||||
msg = _('Create volume from snapshot does not have support '
|
||||
'for virtual pool A and B.')
|
||||
LOG.error(msg)
|
||||
raise exception.InvalidInput(reason=msg)
|
||||
self.get_volume_stats(True)
|
||||
self._assert_enough_space_for_copy(volume['size'])
|
||||
LOG.debug("Creating Volume from snapshot %(source_id)s to "
|
||||
"(%(dest_id)s)", {'source_id': snapshot['id'],
|
||||
'dest_id': volume['id'], })
|
||||
|
||||
orig_name = self._get_snap_name(snapshot['id'])
|
||||
dest_name = self._get_vol_name(volume['id'])
|
||||
self.client_login()
|
||||
try:
|
||||
self.client.copy_volume(orig_name, dest_name, 0, self.backend_name)
|
||||
return None
|
||||
except exception.DotHillRequestError as ex:
|
||||
LOG.exception(_LE("Create volume failed from snapshot: %s"),
|
||||
snapshot['id'])
|
||||
raise exception.Invalid(ex)
|
||||
finally:
|
||||
self.client_logout()
|
||||
|
||||
def delete_volume(self, volume):
|
||||
LOG.debug("Deleting Volume: %s", volume['id'])
|
||||
if volume['name_id']:
|
||||
volume_name = self._get_vol_name(volume['name_id'])
|
||||
else:
|
||||
volume_name = self._get_vol_name(volume['id'])
|
||||
|
||||
self.client_login()
|
||||
try:
|
||||
self.client.delete_volume(volume_name)
|
||||
except exception.DotHillRequestError as ex:
|
||||
# if the volume wasn't found, ignore the error
|
||||
if 'The volume was not found on this system.' in ex:
|
||||
return
|
||||
LOG.exception(_LE("Deletion of volume %s failed."), volume['id'])
|
||||
raise exception.Invalid(ex)
|
||||
finally:
|
||||
self.client_logout()
|
||||
|
||||
def get_volume_stats(self, refresh):
|
||||
if refresh:
|
||||
self.client_login()
|
||||
try:
|
||||
self._update_volume_stats()
|
||||
finally:
|
||||
self.client_logout()
|
||||
return self.stats
|
||||
|
||||
def _update_volume_stats(self):
|
||||
# storage_protocol and volume_backend_name are
|
||||
# set in the child classes
|
||||
stats = {'driver_version': self.VERSION,
|
||||
'storage_protocol': None,
|
||||
'vendor_name': self.vendor_name,
|
||||
'volume_backend_name': None,
|
||||
'pools': []}
|
||||
|
||||
pool = {'QoS_support': False}
|
||||
try:
|
||||
src_type = "%sVolumeDriver" % self.vendor_name
|
||||
backend_stats = self.client.backend_stats(self.backend_name,
|
||||
self.backend_type)
|
||||
pool.update(backend_stats)
|
||||
if (self.backend_type == "linear" or
|
||||
(self.backend_type == "realstor" and
|
||||
self.backend_name not in ['A', 'B'])):
|
||||
pool['location_info'] = ('%s:%s:%s:%s' %
|
||||
(src_type,
|
||||
self.serialNumber,
|
||||
self.backend_name,
|
||||
self.owner))
|
||||
pool['pool_name'] = self.backend_name
|
||||
except exception.DotHillRequestError:
|
||||
err = (_("Unable to get stats for backend_name: %s") %
|
||||
self.backend_name)
|
||||
LOG.exception(err)
|
||||
raise exception.Invalid(reason=err)
|
||||
|
||||
stats['pools'].append(pool)
|
||||
self.stats = stats
|
||||
|
||||
def _assert_connector_ok(self, connector, connector_element):
|
||||
if not connector[connector_element]:
|
||||
msg = _("Connector does not provide: %s") % connector_element
|
||||
LOG.error(msg)
|
||||
raise exception.InvalidInput(reason=msg)
|
||||
|
||||
def map_volume(self, volume, connector, connector_element):
|
||||
self._assert_connector_ok(connector, connector_element)
|
||||
if volume['name_id']:
|
||||
volume_name = self._get_vol_name(volume['name_id'])
|
||||
else:
|
||||
volume_name = self._get_vol_name(volume['id'])
|
||||
try:
|
||||
data = self.client.map_volume(volume_name,
|
||||
connector,
|
||||
connector_element)
|
||||
return data
|
||||
except exception.DotHillRequestError as ex:
|
||||
LOG.exception(_LE("Error mapping volume: %s"), volume_name)
|
||||
raise exception.Invalid(ex)
|
||||
|
||||
def unmap_volume(self, volume, connector, connector_element):
|
||||
self._assert_connector_ok(connector, connector_element)
|
||||
if volume['name_id']:
|
||||
volume_name = self._get_vol_name(volume['name_id'])
|
||||
else:
|
||||
volume_name = self._get_vol_name(volume['id'])
|
||||
|
||||
self.client_login()
|
||||
try:
|
||||
self.client.unmap_volume(volume_name,
|
||||
connector,
|
||||
connector_element)
|
||||
except exception.DotHillRequestError as ex:
|
||||
LOG.exception(_LE("Error unmapping volume: %s"), volume_name)
|
||||
raise exception.Invalid(ex)
|
||||
finally:
|
||||
self.client_logout()
|
||||
|
||||
def get_active_fc_target_ports(self):
|
||||
try:
|
||||
return self.client.get_active_fc_target_ports()
|
||||
except exception.DotHillRequestError as ex:
|
||||
LOG.exception(_LE("Error getting active FC target ports."))
|
||||
raise exception.Invalid(ex)
|
||||
|
||||
def get_active_iscsi_target_iqns(self):
|
||||
try:
|
||||
return self.client.get_active_iscsi_target_iqns()
|
||||
except exception.DotHillRequestError as ex:
|
||||
LOG.exception(_LE("Error getting active ISCSI target iqns."))
|
||||
raise exception.Invalid(ex)
|
||||
|
||||
def get_active_iscsi_target_portals(self):
|
||||
try:
|
||||
return self.client.get_active_iscsi_target_portals(
|
||||
self.backend_type)
|
||||
except exception.DotHillRequestError as ex:
|
||||
LOG.exception(_LE("Error getting active ISCSI target portals."))
|
||||
raise exception.Invalid(ex)
|
||||
|
||||
def create_snapshot(self, snapshot):
|
||||
LOG.debug("Creating snapshot (%(snap_id)s) from %(volume_id)s)",
|
||||
{'snap_id': snapshot['id'],
|
||||
'volume_id': snapshot['volume_id'], })
|
||||
if snapshot['volume']['name_id']:
|
||||
vol_name = self._get_vol_name(snapshot['volume']['name_id'])
|
||||
else:
|
||||
vol_name = self._get_vol_name(snapshot['volume_id'])
|
||||
snap_name = self._get_snap_name(snapshot['id'])
|
||||
|
||||
self.client_login()
|
||||
try:
|
||||
self.client.create_snapshot(vol_name, snap_name)
|
||||
except exception.DotHillRequestError as ex:
|
||||
LOG.exception(_LE("Creation of snapshot failed for volume: %s"),
|
||||
snapshot['volume_id'])
|
||||
raise exception.Invalid(ex)
|
||||
finally:
|
||||
self.client_logout()
|
||||
|
||||
def delete_snapshot(self, snapshot):
|
||||
snap_name = self._get_snap_name(snapshot['id'])
|
||||
LOG.debug("Deleting snapshot (%s)", snapshot['id'])
|
||||
|
||||
self.client_login()
|
||||
try:
|
||||
self.client.delete_snapshot(snap_name)
|
||||
except exception.DotHillRequestError as ex:
|
||||
# if the volume wasn't found, ignore the error
|
||||
if 'The volume was not found on this system.' in ex:
|
||||
return
|
||||
LOG.exception(_LE("Deleting snapshot %s failed"), snapshot['id'])
|
||||
raise exception.Invalid(ex)
|
||||
finally:
|
||||
self.client_logout()
|
||||
|
||||
def extend_volume(self, volume, new_size):
|
||||
if volume['name_id']:
|
||||
volume_name = self._get_vol_name(volume['name_id'])
|
||||
else:
|
||||
volume_name = self._get_vol_name(volume['id'])
|
||||
old_size = volume['size']
|
||||
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.",
|
||||
{'volume_name': volume_name,
|
||||
'old_size': old_size,
|
||||
'new_size': new_size,
|
||||
'growth_size': growth_size, })
|
||||
self.client_login()
|
||||
try:
|
||||
self.client.extend_volume(volume_name, "%dGB" % growth_size)
|
||||
except exception.DotHillRequestError as ex:
|
||||
LOG.exception(_LE("Extension of volume %s failed."), volume['id'])
|
||||
raise exception.Invalid(ex)
|
||||
finally:
|
||||
self.client_logout()
|
||||
|
||||
def get_chap_record(self, initiator_name):
|
||||
try:
|
||||
return self.client.get_chap_record(initiator_name)
|
||||
except exception.DotHillRequestError as ex:
|
||||
LOG.exception(_LE("Error getting chap record."))
|
||||
raise exception.Invalid(ex)
|
||||
|
||||
def create_chap_record(self, initiator_name, chap_secret):
|
||||
try:
|
||||
self.client.create_chap_record(initiator_name, chap_secret)
|
||||
except exception.DotHillRequestError as ex:
|
||||
LOG.exception(_LE("Error creating chap record."))
|
||||
raise exception.Invalid(ex)
|
||||
|
||||
def migrate_volume(self, volume, host):
|
||||
"""Migrate directly if source and dest are managed by same storage.
|
||||
|
||||
:param volume: A dictionary describing the volume to migrate
|
||||
:param host: A dictionary describing the host to migrate to, where
|
||||
host['host'] is its name, and host['capabilities'] is a
|
||||
dictionary of its reported capabilities.
|
||||
:returns (False, None) if the driver does not support migration,
|
||||
(True, None) if successful
|
||||
|
||||
"""
|
||||
false_ret = (False, None)
|
||||
if volume['attach_status'] == "attached":
|
||||
return false_ret
|
||||
if 'location_info' not in host['capabilities']:
|
||||
return false_ret
|
||||
info = host['capabilities']['location_info']
|
||||
try:
|
||||
(dest_type, dest_id,
|
||||
dest_back_name, dest_owner) = info.split(':')
|
||||
except ValueError:
|
||||
return false_ret
|
||||
|
||||
if not (dest_type == 'DotHillVolumeDriver' and
|
||||
dest_id == self.serialNumber and
|
||||
dest_owner == self.owner):
|
||||
return false_ret
|
||||
if volume['name_id']:
|
||||
source_name = self._get_vol_name(volume['name_id'])
|
||||
else:
|
||||
source_name = self._get_vol_name(volume['id'])
|
||||
# DotHill Array does not support duplicate names
|
||||
dest_name = "m%s" % source_name[1:]
|
||||
|
||||
self.client_login()
|
||||
try:
|
||||
self.client.copy_volume(source_name, dest_name, 1, dest_back_name)
|
||||
self.client.delete_volume(source_name)
|
||||
self.client.modify_volume_name(dest_name, source_name)
|
||||
return (True, None)
|
||||
except exception.DotHillRequestError as ex:
|
||||
LOG.exception(_LE("Error migrating volume: %s"), source_name)
|
||||
raise exception.Invalid(ex)
|
||||
finally:
|
||||
self.client_logout()
|
||||
|
||||
def retype(self, volume, new_type, diff, host):
|
||||
ret = self.migrate_volume(volume, host)
|
||||
return ret[0]
|
||||
|
||||
def manage_existing(self, volume, existing_ref):
|
||||
"""Manage an existing non-openstack DotHill volume
|
||||
|
||||
existing_ref is a dictionary of the form:
|
||||
{'source-name': <name of the existing DotHill volume>}
|
||||
"""
|
||||
target_vol_name = existing_ref['source-name']
|
||||
modify_target_vol_name = self._get_vol_name(volume['id'])
|
||||
|
||||
self.client_login()
|
||||
try:
|
||||
self.client.modify_volume_name(target_vol_name,
|
||||
modify_target_vol_name)
|
||||
except exception.DotHillRequestError as ex:
|
||||
LOG.exception(_LE("Error manage existing volume."))
|
||||
raise exception.Invalid(ex)
|
||||
finally:
|
||||
self.client_logout()
|
||||
|
||||
def manage_existing_get_size(self, volume, existing_ref):
|
||||
"""Return size of volume to be managed by manage_existing.
|
||||
|
||||
existing_ref is a dictionary of the form:
|
||||
{'source-name': <name of the volume>}
|
||||
"""
|
||||
target_vol_name = existing_ref['source-name']
|
||||
|
||||
self.client_login()
|
||||
try:
|
||||
size = self.client.get_volume_size(target_vol_name)
|
||||
return size
|
||||
except exception.DotHillRequestError as ex:
|
||||
LOG.exception(_LE("Error manage existing get volume size."))
|
||||
raise exception.Invalid(ex)
|
||||
finally:
|
||||
self.client_logout()
|
172
cinder/volume/drivers/dothill/dothill_fc.py
Normal file
172
cinder/volume/drivers/dothill/dothill_fc.py
Normal file
@ -0,0 +1,172 @@
|
||||
# Copyright 2014 Objectif Libre
|
||||
# Copyright 2015 DotHill Systems
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
from oslo_log import log as logging
|
||||
|
||||
import cinder.volume.driver
|
||||
from cinder.volume.drivers.dothill import dothill_common
|
||||
from cinder.volume.drivers.san import san
|
||||
from cinder.zonemanager import utils as fczm_utils
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DotHillFCDriver(cinder.volume.driver.FibreChannelDriver):
|
||||
"""Openstack Fibre Channel cinder drivers for DotHill Arrays.
|
||||
|
||||
Version history:
|
||||
0.1 - Base version developed for HPMSA FC drivers:
|
||||
"https://github.com/openstack/cinder/tree/stable/juno/
|
||||
cinder/volume/drivers/san/hp"
|
||||
1.0 - Version developed for DotHill arrays with the following
|
||||
modifications:
|
||||
- added support for v3 API(realstor feature)
|
||||
- added support for retype volume
|
||||
- added support for manage/unmanage volume
|
||||
- added initiator target mapping in FC zoning
|
||||
- added https support
|
||||
|
||||
"""
|
||||
|
||||
VERSION = "1.0"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(DotHillFCDriver, self).__init__(*args, **kwargs)
|
||||
self.common = None
|
||||
self.configuration.append_config_values(dothill_common.common_opt)
|
||||
self.configuration.append_config_values(san.san_opts)
|
||||
self.lookup_service = fczm_utils.create_lookup_service()
|
||||
|
||||
def _init_common(self):
|
||||
return dothill_common.DotHillCommon(self.configuration)
|
||||
|
||||
def _check_flags(self):
|
||||
required_flags = ['san_ip', 'san_login', 'san_password']
|
||||
self.common.check_flags(self.configuration, required_flags)
|
||||
|
||||
def do_setup(self, context):
|
||||
self.common = self._init_common()
|
||||
self._check_flags()
|
||||
self.common.do_setup(context)
|
||||
|
||||
def check_for_setup_error(self):
|
||||
self._check_flags()
|
||||
|
||||
def create_volume(self, volume):
|
||||
return {'metadata': self.common.create_volume(volume)}
|
||||
|
||||
def create_volume_from_snapshot(self, volume, src_vref):
|
||||
self.common.create_volume_from_snapshot(volume, src_vref)
|
||||
|
||||
def create_cloned_volume(self, volume, src_vref):
|
||||
return {'metadata': self.common.create_cloned_volume(volume, src_vref)}
|
||||
|
||||
def delete_volume(self, volume):
|
||||
self.common.delete_volume(volume)
|
||||
|
||||
@fczm_utils.AddFCZone
|
||||
def initialize_connection(self, volume, connector):
|
||||
self.common.client_login()
|
||||
try:
|
||||
data = {}
|
||||
data['target_lun'] = self.common.map_volume(volume,
|
||||
connector,
|
||||
'wwpns')
|
||||
|
||||
ports, init_targ_map = self.get_init_targ_map(connector)
|
||||
data['target_discovered'] = True
|
||||
data['target_wwn'] = ports
|
||||
data['initiator_target_map'] = init_targ_map
|
||||
info = {'driver_volume_type': 'fibre_channel',
|
||||
'data': data}
|
||||
return info
|
||||
finally:
|
||||
self.common.client_logout()
|
||||
|
||||
@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
|
||||
|
||||
def get_init_targ_map(self, connector):
|
||||
init_targ_map = {}
|
||||
target_wwns = []
|
||||
ports = self.common.get_active_fc_target_ports()
|
||||
if self.lookup_service is not None:
|
||||
dev_map = self.lookup_service.get_device_mapping_from_network(
|
||||
connector['wwpns'],
|
||||
ports)
|
||||
for fabric_name in dev_map:
|
||||
fabric = dev_map[fabric_name]
|
||||
target_wwns += fabric['target_port_wwn_list']
|
||||
for initiator in fabric['initiator_port_wwn_list']:
|
||||
if initiator not in init_targ_map:
|
||||
init_targ_map[initiator] = []
|
||||
init_targ_map[initiator] += fabric['target_port_wwn_list']
|
||||
init_targ_map[initiator] = list(set(
|
||||
init_targ_map[initiator]))
|
||||
target_wwns = list(set(target_wwns))
|
||||
else:
|
||||
initiator_wwns = connector['wwpns']
|
||||
target_wwns = ports
|
||||
for initiator in initiator_wwns:
|
||||
init_targ_map[initiator] = target_wwns
|
||||
|
||||
return target_wwns, init_targ_map
|
||||
|
||||
def get_volume_stats(self, refresh=False):
|
||||
stats = self.common.get_volume_stats(refresh)
|
||||
stats['storage_protocol'] = 'FC'
|
||||
stats['driver_version'] = self.VERSION
|
||||
backend_name = self.configuration.safe_get('volume_backend_name')
|
||||
stats['volume_backend_name'] = (backend_name or
|
||||
self.__class__.__name__)
|
||||
return stats
|
||||
|
||||
def create_export(self, context, volume):
|
||||
pass
|
||||
|
||||
def ensure_export(self, context, volume):
|
||||
pass
|
||||
|
||||
def remove_export(self, context, volume):
|
||||
pass
|
||||
|
||||
def create_snapshot(self, snapshot):
|
||||
self.common.create_snapshot(snapshot)
|
||||
|
||||
def delete_snapshot(self, snapshot):
|
||||
self.common.delete_snapshot(snapshot)
|
||||
|
||||
def extend_volume(self, volume, new_size):
|
||||
self.common.extend_volume(volume, new_size)
|
||||
|
||||
def retype(self, context, volume, new_type, diff, host):
|
||||
return self.common.retype(volume, new_type, diff, host)
|
||||
|
||||
def manage_existing(self, volume, existing_ref):
|
||||
self.common.manage_existing(volume, existing_ref)
|
||||
|
||||
def manage_existing_get_size(self, volume, existing_ref):
|
||||
return self.common.manage_existing_get_size(volume, existing_ref)
|
||||
|
||||
def unmanage(self, volume):
|
||||
pass
|
195
cinder/volume/drivers/dothill/dothill_iscsi.py
Normal file
195
cinder/volume/drivers/dothill/dothill_iscsi.py
Normal file
@ -0,0 +1,195 @@
|
||||
# Copyright 2014 Objectif Libre
|
||||
# Copyright 2015 DotHill Systems
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
from oslo_log import log as logging
|
||||
|
||||
from cinder import exception
|
||||
from cinder.i18n import _
|
||||
import cinder.volume.driver
|
||||
from cinder.volume.drivers.dothill import dothill_common as dothillcommon
|
||||
from cinder.volume.drivers.san import san
|
||||
|
||||
|
||||
DEFAULT_ISCSI_PORT = "3260"
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DotHillISCSIDriver(cinder.volume.driver.ISCSIDriver):
|
||||
"""Openstack iSCSI cinder drivers for DotHill Arrays.
|
||||
|
||||
Version history:
|
||||
0.1 - Base structure for DotHill iSCSI drivers based on HPMSA FC
|
||||
drivers:
|
||||
"https://github.com/openstack/cinder/tree/stable/juno/
|
||||
cinder/volume/drivers/san/hp"
|
||||
1.0 - Version developed for DotHill arrays with the following
|
||||
modifications:
|
||||
- added iSCSI support
|
||||
- added CHAP support in iSCSI
|
||||
- added support for v3 API(realstor feature)
|
||||
- added support for retype volume
|
||||
- added support for manage/unmanage volume
|
||||
- added https support
|
||||
|
||||
"""
|
||||
|
||||
VERSION = "1.0"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(DotHillISCSIDriver, self).__init__(*args, **kwargs)
|
||||
self.common = None
|
||||
self.configuration.append_config_values(dothillcommon.common_opt)
|
||||
self.configuration.append_config_values(dothillcommon.iscsi_opt)
|
||||
self.configuration.append_config_values(san.san_opts)
|
||||
self.iscsi_ips = self.configuration.dothill_iscsi_ips
|
||||
|
||||
def _init_common(self):
|
||||
return dothillcommon.DotHillCommon(self.configuration)
|
||||
|
||||
def _check_flags(self):
|
||||
required_flags = ['san_ip', 'san_login', 'san_password']
|
||||
self.common.check_flags(self.configuration, required_flags)
|
||||
|
||||
def do_setup(self, context):
|
||||
self.common = self._init_common()
|
||||
self._check_flags()
|
||||
self.common.do_setup(context)
|
||||
self.initialize_iscsi_ports()
|
||||
|
||||
def initialize_iscsi_ports(self):
|
||||
iscsi_ips = []
|
||||
if self.iscsi_ips:
|
||||
for ip_addr in self.iscsi_ips:
|
||||
ip = ip_addr.split(':')
|
||||
if len(ip) == 1:
|
||||
iscsi_ips.append([ip_addr, DEFAULT_ISCSI_PORT])
|
||||
elif len(ip) == 2:
|
||||
iscsi_ips.append([ip[0], ip[1]])
|
||||
else:
|
||||
msg = _("Invalid IP address format: '%s'") % ip_addr
|
||||
LOG.error(msg)
|
||||
raise exception.InvalidInput(reason=(msg))
|
||||
self.iscsi_ips = iscsi_ips
|
||||
else:
|
||||
msg = _('At least one valid iSCSI IP address must be set.')
|
||||
LOG.error(msg)
|
||||
raise exception.InvalidInput(reason=(msg))
|
||||
|
||||
def check_for_setup_error(self):
|
||||
self._check_flags()
|
||||
|
||||
def create_volume(self, volume):
|
||||
return {'metadata': self.common.create_volume(volume)}
|
||||
|
||||
def create_volume_from_snapshot(self, volume, src_vref):
|
||||
self.common.create_volume_from_snapshot(volume, src_vref)
|
||||
|
||||
def create_cloned_volume(self, volume, src_vref):
|
||||
return {'metadata': self.common.create_cloned_volume(volume, src_vref)}
|
||||
|
||||
def delete_volume(self, volume):
|
||||
self.common.delete_volume(volume)
|
||||
|
||||
def initialize_connection(self, volume, connector):
|
||||
self.common.client_login()
|
||||
try:
|
||||
data = {}
|
||||
data['target_lun'] = self.common.map_volume(volume,
|
||||
connector,
|
||||
'initiator')
|
||||
iqns = self.common.get_active_iscsi_target_iqns()
|
||||
data['target_discovered'] = True
|
||||
data['target_iqn'] = iqns[0]
|
||||
iscsi_portals = self.common.get_active_iscsi_target_portals()
|
||||
|
||||
for ip_port in self.iscsi_ips:
|
||||
if (ip_port[0] in iscsi_portals):
|
||||
data['target_portal'] = ":".join(ip_port)
|
||||
break
|
||||
|
||||
if 'target_portal' not in data:
|
||||
raise exception.DotHillNotTargetPortal()
|
||||
|
||||
if self.configuration.use_chap_auth:
|
||||
chap_secret = self.common.get_chap_record(
|
||||
connector['initiator']
|
||||
)
|
||||
if not chap_secret:
|
||||
chap_secret = self.create_chap_record(
|
||||
connector['initiator']
|
||||
)
|
||||
data['auth_password'] = chap_secret
|
||||
data['auth_username'] = connector['initiator']
|
||||
data['auth_method'] = 'CHAP'
|
||||
|
||||
info = {'driver_volume_type': 'iscsi',
|
||||
'data': data}
|
||||
return info
|
||||
finally:
|
||||
self.common.client_logout()
|
||||
|
||||
def terminate_connection(self, volume, connector, **kwargs):
|
||||
self.common.unmap_volume(volume, connector, 'initiator')
|
||||
|
||||
def get_volume_stats(self, refresh=False):
|
||||
stats = self.common.get_volume_stats(refresh)
|
||||
stats['storage_protocol'] = 'iSCSI'
|
||||
stats['driver_version'] = self.VERSION
|
||||
backend_name = self.configuration.safe_get('volume_backend_name')
|
||||
stats['volume_backend_name'] = (backend_name or
|
||||
self.__class__.__name__)
|
||||
return stats
|
||||
|
||||
def create_export(self, context, volume):
|
||||
pass
|
||||
|
||||
def ensure_export(self, context, volume):
|
||||
pass
|
||||
|
||||
def remove_export(self, context, volume):
|
||||
pass
|
||||
|
||||
def create_snapshot(self, snapshot):
|
||||
self.common.create_snapshot(snapshot)
|
||||
|
||||
def delete_snapshot(self, snapshot):
|
||||
self.common.delete_snapshot(snapshot)
|
||||
|
||||
def extend_volume(self, volume, new_size):
|
||||
self.common.extend_volume(volume, new_size)
|
||||
|
||||
def create_chap_record(self, initiator_name):
|
||||
chap_secret = self.configuration.chap_password
|
||||
# Chap secret length should be 12 to 16 characters
|
||||
if 12 <= len(chap_secret) <= 16:
|
||||
self.common.create_chap_record(initiator_name, chap_secret)
|
||||
else:
|
||||
msg = _('CHAP secret should be 12-16 bytes.')
|
||||
LOG.error(msg)
|
||||
raise exception.InvalidInput(reason=(msg))
|
||||
return chap_secret
|
||||
|
||||
def retype(self, context, volume, new_type, diff, host):
|
||||
return self.common.retype(volume, new_type, diff, host)
|
||||
|
||||
def manage_existing(self, volume, existing_ref):
|
||||
self.common.manage_existing(volume, existing_ref)
|
||||
|
||||
def manage_existing_get_size(self, volume, existing_ref):
|
||||
return self.common.manage_existing_get_size(volume, existing_ref)
|
||||
|
||||
def unmanage(self, volume):
|
||||
pass
|
Loading…
Reference in New Issue
Block a user