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:
nikeshm 2015-04-27 12:23:11 +05:30
parent eebc694df2
commit 7f7f13bcac
7 changed files with 2003 additions and 0 deletions

View File

@ -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")

View 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)

View 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)

View 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()

View 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

View 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