Add HP MSA Fiber Channel driver

This commit implements a driver for the HP MSA arrays. Only Fiber
Channel is supported for now.

Implements: blueprint add-msa-2040-driver
DocImpact
Change-Id: I75232cafadf7f6d15c5959d6c054f2e0f4d14e2c
This commit is contained in:
Gauvain Pocentek 2014-02-13 21:34:59 +01:00
parent df852cfe7f
commit 1586b2f947
6 changed files with 1287 additions and 0 deletions

View File

@ -672,3 +672,20 @@ class GlusterfsNoSuitableShareFound(VolumeDriverException):
class RemoveExportException(VolumeDriverException):
message = _("Failed to remove export for volume %(volume)s: %(reason)s")
# HP MSA
class HPMSAVolumeDriverException(VolumeDriverException):
message = _("HP MSA Volume Driver exception")
class HPMSAInvalidVDisk(HPMSAVolumeDriverException):
message = _("VDisk doesn't exist (%(vdisk)s)")
class HPMSAConnectionError(HPMSAVolumeDriverException):
message = _("Unable to connect to MSA array")
class HPMSANotEnoughSpace(HPMSAVolumeDriverException):
message = _("Not enough space on VDisk (%(vdisk)s)")

549
cinder/tests/test_hp_msa.py Normal file
View File

@ -0,0 +1,549 @@
# (c) Copyright 2014 Objectif Libre
#
# 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 HP MSA driver."""
import lxml.etree as etree
import mock
import urllib2
from cinder import exception
from cinder import test
from cinder.volume.drivers.san.hp import hp_msa_client as msa
from cinder.volume.drivers.san.hp import hp_msa_common
from cinder.volume.drivers.san.hp import hp_msa_fc
session_key = 'JSESS0004eb8a82b08fd5'
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">JSESS0004eb8a82b08fd5</PROPERTY>
<PROPERTY name="return-code">1</PROPERTY></OBJECT></RESPONSE>'''
resp_badlogin = '''<RESPONSE><OBJECT basetype="status" name="status" oid="1">
</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 = '''<RESPONSE><OBJECT basetype="virtual-disks">
<PROPERTY name="size-numeric">1756381184</PROPERTY>
<PROPERTY name="freespace-numeric">756381184</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">3</PROPERTY></OBJECT></RESPONSE>'''
response_ports = '''<RESPONSE><OBJECT basetype="port">
<PROPERTY name="port-type">FC</PROPERTY>
<PROPERTY name="target-id">id1</PROPERTY>
<PROPERTY name="status">Up</PROPERTY></OBJECT>
<OBJECT basetype="port">
<PROPERTY name="port-type">FC</PROPERTY>
<PROPERTY name="target-id">id2</PROPERTY>
<PROPERTY name="status">Disconnected</PROPERTY></OBJECT>
<OBJECT basetype="port">
<PROPERTY name="port-type">iSCSI</PROPERTY>
<PROPERTY name="target-id">id3</PROPERTY>
<PROPERTY name="status">Up</PROPERTY></OBJECT></RESPONSE>'''
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 = 'ecffc30f-98cb-4cf5-85ee-d7309cc17cd2'
test_volume = {'id': vol_id,
'display_name': 'test volume', 'name': 'volume', 'size': 10}
test_snap = {'id': 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
'volume_id': vol_id,
'display_name': 'test volume', 'name': 'volume', 'size': 10}
encoded_volid = 'v7P_DD5jLTPWF7tcwnMF'
encoded_snapid = 's7P_DD5jLTPWF7tcwnMF'
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,
'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': 'iqn.1993-08.org.debian:01:222',
'wwpns': [],
'wwnns': [],
'host': 'fakehost'}
class TestHPMSAClient(test.TestCase):
def setUp(self):
super(TestHPMSAClient, self).setUp()
self.login = 'manage'
self.passwd = '!manage'
self.ip = '10.0.0.1'
self.client = msa.HPMSAClient(self.ip, self.login, self.passwd)
@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(self.client._session_key, session_key)
m.read.side_effect = [resp_badlogin]
self.assertRaises(msa.HPMSAAuthenticationError,
self.client.login)
def test_build_request_url(self):
url = self.client._build_request_url('/path', None)
self.assertEqual(url, 'http://10.0.0.1/api/path')
url = self.client._build_request_url('/path', None, arg1='val1')
self.assertEqual(url, 'http://10.0.0.1/api/path/arg1/val1')
url = self.client._build_request_url('/path', 'arg1')
self.assertEqual(url, 'http://10.0.0.1/api/path/arg1')
url = self.client._build_request_url('/path', 'arg1', arg2='val2')
self.assertEqual(url, 'http://10.0.0.1/api/path/arg2/val2/arg1')
url = self.client._build_request_url('/path', ['arg1', 'arg3'],
arg2='val2')
self.assertEqual(url, 'http://10.0.0.1/api/path/arg2/val2/arg1/arg3')
@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', None)
self.assertTrue(type(ret) == etree._Element)
self.assertRaises(msa.HPMSAConnectionError, self.client._request,
'/path', None)
self.assertRaises(msa.HPMSAConnectionError, self.client._request,
'/path', None)
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(ret, None)
self.assertRaises(msa.HPMSARequestError,
self.client._assert_response_ok, not_ok_tree)
self.assertRaises(msa.HPMSARequestError,
self.client._assert_response_ok, invalid_tree)
@mock.patch.object(msa.HPMSAClient, '_request')
def test_vdisk_exists(self, mock_request):
mock_request.side_effect = [msa.HPMSARequestError,
fake_xml]
self.assertEqual(self.client.vdisk_exists('vdisk'), False)
self.assertEqual(self.client.vdisk_exists('vdisk'), True)
@mock.patch.object(msa.HPMSAClient, '_request')
def test_vdisk_stats(self, mock_request):
mock_request.return_value = etree.XML(response_stats)
ret = self.client.vdisk_stats('OpenStack')
self.assertEqual(ret, {'free_capacity_gb': 387,
'total_capacity_gb': 899})
mock_request.assert_called_with('/show/vdisks', 'OpenStack')
@mock.patch.object(msa.HPMSAClient, '_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(ret, 1)
ret = self.client._get_first_available_lun_for_host("fakehost")
self.assertEqual(ret, 2)
@mock.patch.object(msa.HPMSAClient, '_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(ret, [{'port-type': 'FC',
'target-id': 'id1',
'status': 'Up'},
{'port-type': 'iSCSI',
'target-id': 'id3',
'status': 'Up'}])
@mock.patch.object(msa.HPMSAClient, '_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(ret, ['id1'])
class FakeConfiguration(object):
msa_vdisk = 'OpenStack'
san_ip = '10.0.0.1'
san_login = 'manage'
san_password = '!manage'
def safe_get(self, key):
return 'fakevalue'
class TestHPMSACommon(test.TestCase):
def setUp(self):
super(TestHPMSACommon, self).setUp()
self.config = FakeConfiguration()
self.common = hp_msa_common.HPMSACommon(self.config)
@mock.patch.object(msa.HPMSAClient, 'vdisk_exists')
@mock.patch.object(msa.HPMSAClient, 'logout')
@mock.patch.object(msa.HPMSAClient, 'login')
def test_do_setup(self, mock_login, mock_logout, mock_vdisk_exists):
mock_login.side_effect = [msa.HPMSAConnectionError,
msa.HPMSAAuthenticationError,
None, None]
mock_vdisk_exists.side_effect = [False, True]
mock_logout.return_value = None
self.assertRaises(exception.HPMSAConnectionError,
self.common.do_setup, None)
self.assertRaises(exception.HPMSAConnectionError,
self.common.do_setup, None)
self.assertRaises(exception.HPMSAInvalidVDisk, self.common.do_setup,
None)
mock_vdisk_exists.assert_called_with(self.config.msa_vdisk)
self.assertEqual(self.common.do_setup(None), None)
mock_vdisk_exists.assert_called_with(self.config.msa_vdisk)
mock_logout.assert_called_with()
def test_vol_name(self):
self.assertEqual(self.common._get_vol_name(vol_id), encoded_volid)
self.assertEqual(self.common._get_snap_name(vol_id),
encoded_snapid)
def test_check_flags(self):
class FakeOptions():
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(ret, None)
options = FakeOptions({'opt1': 'val1', 'opt3': 'val3'})
required_flags = ['opt1', 'opt2']
self.assertEqual(ret, None)
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.assertEqual(None,
self.common._assert_connector_ok(connector))
@mock.patch.object(msa.HPMSAClient, 'vdisk_stats')
def test_update_volume_stats(self, mock_stats):
mock_stats.side_effect = [msa.HPMSARequestError,
stats_large_space]
self.assertRaises(exception.Invalid, self.common._update_volume_stats)
mock_stats.assert_called_with(self.config.msa_vdisk)
ret = self.common._update_volume_stats()
self.assertEqual(ret, None)
self.assertEqual(self.common.stats,
{'storage_protocol': None,
'vendor_name': 'Hewlett-Packard',
'driver_version': self.common.VERSION,
'volume_backend_name': None,
'free_capacity_gb': 90,
'reserved_percentage': 0,
'total_capacity_gb': 100,
'QoS_support': False})
@mock.patch.object(msa.HPMSAClient, 'create_volume')
def test_create_volume(self, mock_create):
mock_create.side_effect = [msa.HPMSARequestError, None]
self.assertRaises(exception.Invalid, self.common.create_volume,
test_volume)
ret = self.common.create_volume(test_volume)
self.assertEqual(ret, None)
mock_create.assert_called_with(self.common.config.msa_vdisk,
encoded_volid,
"%sGB" % test_volume['size'])
@mock.patch.object(msa.HPMSAClient, 'delete_volume')
def test_delete_volume(self, mock_delete):
not_found_e = msa.HPMSARequestError(
'The volume was not found on this system.')
mock_delete.side_effect = [not_found_e, msa.HPMSARequestError,
None]
self.assertEqual(self.common.delete_volume(test_volume), None)
self.assertRaises(exception.Invalid, self.common.delete_volume,
test_volume)
self.assertEqual(self.common.delete_volume(test_volume), None)
mock_delete.assert_called_with(encoded_volid)
@mock.patch.object(msa.HPMSAClient, 'copy_volume')
@mock.patch.object(msa.HPMSAClient, 'vdisk_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.HPMSANotEnoughSpace,
self.common.create_cloned_volume,
dest_volume, detached_volume)
self.assertFalse(mock_copy.called)
mock_copy.side_effect = [msa.HPMSARequestError, 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(ret, None)
mock_copy.assert_called_with(encoded_volid,
'vqqqqqqqqqqqqqqqqqqq',
self.common.config.msa_vdisk)
@mock.patch.object(msa.HPMSAClient, 'copy_volume')
@mock.patch.object(msa.HPMSAClient, 'vdisk_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.HPMSANotEnoughSpace,
self.common.create_volume_from_snapshot,
dest_volume, test_snap)
mock_copy.side_effect = [msa.HPMSARequestError, 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(ret, None)
mock_copy.assert_called_with('sqqqqqqqqqqqqqqqqqqq',
'vqqqqqqqqqqqqqqqqqqq',
self.common.config.msa_vdisk)
@mock.patch.object(msa.HPMSAClient, 'extend_volume')
def test_extend_volume(self, mock_extend):
mock_extend.side_effect = [msa.HPMSARequestError, None]
self.assertRaises(exception.Invalid, self.common.extend_volume,
test_volume, 20)
ret = self.common.extend_volume(test_volume, 20)
self.assertEqual(ret, None)
mock_extend.assert_called_with(encoded_volid, '10GB')
@mock.patch.object(msa.HPMSAClient, 'create_snapshot')
def test_create_snapshot(self, mock_create):
mock_create.side_effect = [msa.HPMSARequestError, None]
self.assertRaises(exception.Invalid, self.common.create_snapshot,
test_snap)
ret = self.common.create_snapshot(test_snap)
self.assertEqual(ret, None)
mock_create.assert_called_with(encoded_volid, 'sqqqqqqqqqqqqqqqqqqq')
@mock.patch.object(msa.HPMSAClient, 'delete_snapshot')
def test_delete_snapshot(self, mock_delete):
not_found_e = msa.HPMSARequestError(
'The volume was not found on this system.')
mock_delete.side_effect = [not_found_e, msa.HPMSARequestError,
None]
self.assertEqual(self.common.delete_snapshot(test_snap), None)
self.assertRaises(exception.Invalid, self.common.delete_snapshot,
test_snap)
self.assertEqual(self.common.delete_snapshot(test_snap), None)
mock_delete.assert_called_with('sqqqqqqqqqqqqqqqqqqq')
@mock.patch.object(msa.HPMSAClient, 'map_volume')
def test_map_volume(self, mock_map):
mock_map.side_effect = [msa.HPMSARequestError, 10]
self.assertRaises(exception.Invalid, self.common.map_volume,
test_volume, connector)
lun = self.common.map_volume(test_volume, connector)
self.assertEqual(lun, 10)
mock_map.assert_called_with(encoded_volid, connector['wwpns'])
@mock.patch.object(msa.HPMSAClient, 'unmap_volume')
def test_unmap_volume(self, mock_unmap):
mock_unmap.side_effect = [msa.HPMSARequestError, None]
self.assertRaises(exception.Invalid, self.common.unmap_volume,
test_volume, connector)
ret = self.common.unmap_volume(test_volume, connector)
self.assertEqual(ret, None)
mock_unmap.assert_called_with(encoded_volid, connector['wwpns'])
class TestHPMSAFC(test.TestCase):
@mock.patch.object(hp_msa_common.HPMSACommon, 'do_setup')
def setUp(self, mock_setup):
super(TestHPMSAFC, self).setUp()
mock_setup.return_value = True
def fake_init(self, *args, **kwargs):
super(hp_msa_fc.HPMSAFCDriver, self).__init__()
self.common = None
self.configuration = FakeConfiguration()
hp_msa_fc.HPMSAFCDriver.__init__ = fake_init
self.driver = hp_msa_fc.HPMSAFCDriver()
self.driver.do_setup(None)
self.driver.common.client_login = mock.MagicMock(return_value=None)
self.driver.common.client_logout = mock.MagicMock(return_value=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(hp_msa_common.HPMSACommon, 'create_volume')
def test_create_volume(self, mock_create):
self._test_with_mock(mock_create, 'create_volume', [None],
{'metadata': None})
@mock.patch.object(hp_msa_common.HPMSACommon,
'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(hp_msa_common.HPMSACommon,
'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(hp_msa_common.HPMSACommon, 'delete_volume')
def test_delete_volume(self, mock_delete):
self._test_with_mock(mock_delete, 'delete_volume', [None])
@mock.patch.object(hp_msa_common.HPMSACommon, 'create_snapshot')
def test_create_snapshot(self, mock_create):
self._test_with_mock(mock_create, 'create_snapshot', [None])
@mock.patch.object(hp_msa_common.HPMSACommon, 'delete_snapshot')
def test_delete_snapshot(self, mock_delete):
self._test_with_mock(mock_delete, 'delete_snapshot', [None])
@mock.patch.object(hp_msa_common.HPMSACommon, 'extend_volume')
def test_extend_volume(self, mock_extend):
self._test_with_mock(mock_extend, 'extend_volume', [None, 10])
@mock.patch.object(hp_msa_common.HPMSACommon, 'client_logout')
@mock.patch.object(hp_msa_common.HPMSACommon,
'get_active_fc_target_ports')
@mock.patch.object(hp_msa_common.HPMSACommon, 'map_volume')
@mock.patch.object(hp_msa_common.HPMSACommon, '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)
ret = self.driver.initialize_connection(test_volume, connector)
self.assertEqual(ret, {'driver_volume_type': 'fibre_channel',
'data': {'target_wwn': ['id1'],
'target_lun': 1,
'target_discovered': True}})
mock_ports.assert_called_once()
@mock.patch.object(hp_msa_common.HPMSACommon, 'client_logout')
@mock.patch.object(hp_msa_common.HPMSACommon, 'unmap_volume')
@mock.patch.object(hp_msa_common.HPMSACommon, 'client_login')
def test_terminate_connection(self, mock_login, mock_unmap, mock_logout):
mock_login.return_value = None
mock_logout.return_value = None
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)
ret = self.driver.terminate_connection(test_volume, connector)
self.assertEqual(ret, None)
@mock.patch.object(hp_msa_common.HPMSACommon, 'client_logout')
@mock.patch.object(hp_msa_common.HPMSACommon, 'get_volume_stats')
@mock.patch.object(hp_msa_common.HPMSACommon, 'client_login')
def test_get_volume_stats(self, mock_login, mock_stats, mock_logout):
stats = {'storage_protocol': None,
'driver_version': self.driver.VERSION,
'volume_backend_name': None,
'free_capacity_gb': 90,
'reserved_percentage': 0,
'total_capacity_gb': 100,
'QoS_support': False}
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(ret, {'storage_protocol': 'FC',
'driver_version': self.driver.VERSION,
'volume_backend_name': 'fakevalue',
'free_capacity_gb': 90,
'reserved_percentage': 0,
'total_capacity_gb': 100,
'QoS_support': False})
ret = self.driver.get_volume_stats(True)
self.assertEqual(ret, {'storage_protocol': 'FC',
'driver_version': self.driver.VERSION,
'volume_backend_name': 'fakevalue',
'free_capacity_gb': 90,
'reserved_percentage': 0,
'total_capacity_gb': 100,
'QoS_support': False})
mock_stats.assert_called_with(True)

View File

@ -0,0 +1,240 @@
# Copyright 2014 Objectif Libre
#
# 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 urllib2
from lxml import etree
class HPMSAConnectionError(Exception):
pass
class HPMSAAuthenticationError(Exception):
pass
class HPMSARequestError(Exception):
pass
class HPMSAClient(object):
def __init__(self, host, login, password, protocol='http'):
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
obj = etree.XML(xml).find("OBJECT")
for prop in obj.iter("PROPERTY"):
if prop.get("name") == "response":
self._session_key = prop.text
break
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 HPMSAConnectionError()
self._get_auth_token(xml)
if self._session_key is None:
raise HPMSAAuthenticationError()
def _assert_response_ok(self, tree):
"""Parses the XML returned by the device to check the return code.
Raises a HPMSARequestError error if the return code is not 0.
"""
for obj in tree.iter():
if obj.get("basetype") != "status":
continue
ret_code = ret_str = None
for prop in obj.iter("PROPERTY"):
if prop.get("name") == "return-code":
ret_code = prop.text
elif prop.get("name") == "response":
ret_str = prop.text
if ret_code != "0":
raise HPMSARequestError(ret_str)
else:
return
raise HPMSARequestError("No status found")
def _build_request_url(self, path, args=None, **kargs):
url = self._base_url + path
if kargs:
url += '/' + '/'.join(["%s/%s" % (k.replace('_', '-'), v)
for (k, v) in kargs.items()])
if args:
if not isinstance(args, list):
args = [args]
url += '/' + '/'.join(args)
return url
def _request(self, path, args=None, **kargs):
"""Performs an HTTP request on the device.
Raises a HPMSARequestError 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()
except urllib2.URLError:
raise HPMSAConnectionError()
try:
tree = etree.XML(xml)
except etree.LxmlError:
raise HPMSAConnectionError()
self._assert_response_ok(tree)
return tree
def logout(self):
url = self._base_url + '/exit'
try:
urllib2.urlopen(url)
return True
except HPMSARequestError:
return False
def create_volume(self, vdisk, name, size):
# NOTE: size is in this format: [0-9]+GB
self._request("/create/volume", name, vdisk=vdisk, size=size)
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 vdisk_exists(self, vdisk):
try:
self._request("/show/vdisks", vdisk)
return True
except HPMSARequestError:
return False
def vdisk_stats(self, vdisk):
stats = {'free_capacity_gb': 0,
'total_capacity_gb': 0}
tree = self._request("/show/vdisks", vdisk)
for obj in tree.iter():
if obj.get("basetype") != "virtual-disks":
continue
for prop in obj.iter("PROPERTY"):
# the sizes are given in number of blocks of 512 octets
if prop.get("name") == "size-numeric":
stats['total_capacity_gb'] = \
int(prop.text) * 512 / (10 ** 9)
elif prop.get("name") == "freespace-numeric":
stats['free_capacity_gb'] = \
int(prop.text) * 512 / (10 ** 9)
return stats
def _get_first_available_lun_for_host(self, host):
luns = []
tree = self._request("/show/host-maps", host)
for obj in tree.iter():
if obj.get("basetype") != "host-view-mappings":
continue
for prop in obj.iter("PROPERTY"):
if prop.get("name") == "lun":
luns.append(int(prop.text))
lun = 1
while True:
if lun not in luns:
return lun
lun += 1
def map_volume(self, volume_name, wwpns):
# NOTE(gpocentek): we assume that luns will be the same for all hosts
lun = self._get_first_available_lun_for_host(wwpns[0])
hosts = ",".join(wwpns)
self._request("/map/volume", volume_name,
lun=str(lun), host=hosts, access="rw")
return lun
def unmap_volume(self, volume_name, wwpns):
hosts = ",".join(wwpns)
self._request("/unmap/volume", volume_name, host=hosts)
def get_active_target_ports(self):
ports = []
tree = self._request("/show/ports")
for obj in tree.iter():
if obj.get("basetype") != "port":
continue
port = {}
for prop in obj.iter("PROPERTY"):
prop_name = prop.get("name")
if prop_name in ["port-type", "target-id", "status"]:
port[prop_name] = prop.text
if port['status'] != 'Up':
continue
ports.append(port)
return ports
def get_active_fc_target_ports(self):
ports = []
for port in self.get_active_target_ports():
if port['port-type'] == "FC":
ports.append(port['target-id'])
return ports
def copy_volume(self, source_name, target_name, vdisk):
self._request("/volumecopy", target_name,
dest_vdisk=vdisk,
source_volume=source_name,
prompt='yes')

View File

@ -0,0 +1,315 @@
# Copyright 2014 Objectif Libre
#
# 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 HP MSA Storage array
"""
import base64
import uuid
from oslo.config import cfg
from cinder import exception
from cinder.openstack.common import log as logging
from cinder.volume.drivers.san.hp import hp_msa_client as msa
LOG = logging.getLogger(__name__)
hpmsa_opt = [
cfg.StrOpt('msa_vdisk',
default='OpenStack',
help="The VDisk to use for volume creation."),
]
CONF = cfg.CONF
CONF.register_opts(hpmsa_opt)
class HPMSACommon(object):
VERSION = "0.1"
stats = {}
def __init__(self, config):
self.config = config
self.client = msa.HPMSAClient(self.config.san_ip,
self.config.san_login,
self.config.san_password)
self.vdisk = self.config.msa_vdisk
def get_version(self):
return self.VERSION
def do_setup(self, context):
self.client_login()
self._validate_vdisks()
self.client_logout()
def client_login(self):
LOG.debug(_("Connecting to MSA"))
try:
self.client.login()
except msa.HPMSAConnectionError as ex:
msg = (_("Failed to connect to MSA Array (%(host)s): %(err)s") %
{'host': self.config.san_ip, 'err': ex})
LOG.error(msg)
raise exception.HPMSAConnectionError(reason=msg)
except msa.HPMSAAuthenticationError as e:
msg = _("Failed to log on MSA Array (invalid login?)")
LOG.error(msg)
raise exception.HPMSAConnectionError(reason=msg)
def _validate_vdisks(self):
if not self.client.vdisk_exists(self.vdisk):
self.client_logout()
raise exception.HPMSAInvalidVDisk(vdisk=self.vdisk)
def client_logout(self):
self.client.logout()
LOG.debug(_("Disconnected from MSA Array"))
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 MSA volume name.
Converts the openstack volume id from
ecffc30f-98cb-4cf5-85ee-d7309cc17cd2
to
7P_DD5jLTPWF7tcwnMF80g
We convert the 128 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 MSA
vol_encoded = vol_encoded.replace('+', '.')
# since we use http URLs to send paramters, '/' is not an acceptable
# parameter
vol_encoded = vol_encoded.replace('/', '_')
# NOTE(gpocentek): 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):
volume_id = self._get_vol_name(volume['id'])
LOG.debug(_("Create Volume (%(display_name)s: %(name)s %(id)s)") %
{'display_name': volume['display_name'],
'name': volume['name'], 'id': volume_id})
# use base64 to encode the volume name (UUID is too long for MSA)
volume_name = self._get_vol_name(volume['id'])
volume_size = "%dGB" % volume['size']
try:
metadata = self.client.create_volume(self.config.msa_vdisk,
volume_name,
volume_size)
except msa.HPMSARequestError as ex:
LOG.error(ex)
raise exception.Invalid(ex)
return metadata
def _assert_enough_space_for_copy(self, volume_size):
"""The MSA 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['free_capacity_gb']:
raise exception.HPMSANotEnoughSpace(vdisk=self.vdisk)
def _assert_source_detached(self, volume):
"""The MSA 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":
msg = _("Volume must be detached to perform a clone operation.")
LOG.error(msg)
raise exception.VolumeAttached(volume_id=volume['id'])
def create_cloned_volume(self, volume, src_vref):
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 (%(dest_id)s)") %
{'source_id': volume['source_volid'],
'dest_id': volume['id']})
orig_name = self._get_vol_name(volume['source_volid'])
dest_name = self._get_vol_name(volume['id'])
try:
self.client.copy_volume(orig_name, dest_name,
self.config.msa_vdisk)
except msa.HPMSARequestError as ex:
LOG.error(ex)
raise exception.Invalid(ex)
return None
def create_volume_from_snapshot(self, volume, snapshot):
self.get_volume_stats(True)
self._assert_enough_space_for_copy(volume['size'])
LOG.debug(_("Creating Volume from snapshot %(source_id)s "
"(%(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'])
try:
self.client.copy_volume(orig_name, dest_name,
self.config.msa_vdisk)
except msa.HPMSARequestError as ex:
LOG.error(ex)
raise exception.Invalid(ex)
return None
def delete_volume(self, volume):
LOG.debug(_("Deleting Volume (%s)") % volume['id'])
volume_name = self._get_vol_name(volume['id'])
try:
self.client.delete_volume(volume_name)
except msa.HPMSARequestError as ex:
LOG.error(ex)
# if the volume wasn't found, ignore the error
if 'The volume was not found on this system.' in ex:
return
raise exception.Invalid(ex)
def get_volume_stats(self, refresh):
if refresh:
self._update_volume_stats()
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,
'free_capacity_gb': 'unknown',
'reserved_percentage': 0,
'storage_protocol': None,
'total_capacity_gb': 'unknown',
'QoS_support': False,
'vendor_name': 'Hewlett-Packard',
'volume_backend_name': None}
try:
vdisk_stats = self.client.vdisk_stats(self.config.msa_vdisk)
stats.update(vdisk_stats)
except msa.HPMSARequestError:
err = (_("Unable to get stats for VDisk (%s)")
% self.config.msa_vdisk)
LOG.error(err)
raise exception.Invalid(reason=err)
self.stats = stats
def _assert_connector_ok(self, connector):
if not connector['wwpns']:
msg = _("Connector doesn't provide wwpns")
LOG.error(msg)
raise exception.InvalidInput(reason=msg)
def map_volume(self, volume, connector):
self._assert_connector_ok(connector)
volume_name = self._get_vol_name(volume['id'])
try:
data = self.client.map_volume(volume_name, connector['wwpns'])
return data
except msa.HPMSARequestError as ex:
LOG.error(ex)
raise exception.Invalid(ex)
def unmap_volume(self, volume, connector):
self._assert_connector_ok(connector)
volume_name = self._get_vol_name(volume['id'])
try:
self.client.unmap_volume(volume_name, connector['wwpns'])
except msa.HPMSARequestError as ex:
LOG.error(ex)
raise exception.Invalid(ex)
def get_active_fc_target_ports(self):
return self.client.get_active_fc_target_ports()
def create_snapshot(self, snapshot):
LOG.debug(_("Creating Snapshot from %(volume_id)s (%(snap_id)s)") %
{'volume_id': snapshot['volume_id'],
'snap_id': snapshot['id']})
snap_name = self._get_snap_name(snapshot['id'])
vol_name = self._get_vol_name(snapshot['volume_id'])
try:
self.client.create_snapshot(vol_name, snap_name)
except msa.HPMSARequestError as ex:
LOG.error(ex)
raise exception.Invalid(ex)
def delete_snapshot(self, snapshot):
snap_name = self._get_snap_name(snapshot['id'])
LOG.debug(_("Deleting Snapshot (%s)") % snapshot['id'])
try:
self.client.delete_snapshot(snap_name)
except msa.HPMSARequestError as ex:
LOG.error(ex)
# if the volume wasn't found, ignore the error
if 'The volume was not found on this system.' in ex:
return
raise exception.Invalid(ex)
def extend_volume(self, volume, new_size):
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})
try:
self.client.extend_volume(volume_name, "%dGB" % growth_size)
except msa.HPMSARequestError as ex:
LOG.error(ex)
raise exception.Invalid(ex)

View File

@ -0,0 +1,158 @@
# Copyright 2014 Objectif Libre
#
# 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 cinder.openstack.common import log as logging
from cinder import utils
import cinder.volume.driver
from cinder.volume.drivers.san.hp import hp_msa_common as hpcommon
from cinder.volume.drivers.san import san
LOG = logging.getLogger(__name__)
class HPMSAFCDriver(cinder.volume.driver.FibreChannelDriver):
VERSION = "0.1"
def __init__(self, *args, **kwargs):
super(HPMSAFCDriver, self).__init__(*args, **kwargs)
self.common = None
self.configuration.append_config_values(hpcommon.hpmsa_opt)
self.configuration.append_config_values(san.san_opts)
def _init_common(self):
return hpcommon.HPMSACommon(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()
@utils.synchronized('msa', external=True)
def create_volume(self, volume):
self.common.client_login()
try:
metadata = self.common.create_volume(volume)
return {'metadata': metadata}
finally:
self.common.client_logout()
@utils.synchronized('msa', external=True)
def create_volume_from_snapshot(self, volume, src_vref):
self.common.client_login()
try:
self.common.create_volume_from_snapshot(volume, src_vref)
finally:
self.common.client_logout()
@utils.synchronized('msa', external=True)
def create_cloned_volume(self, volume, src_vref):
self.common.client_login()
try:
new_vol = self.common.create_cloned_volume(volume, src_vref)
return {'metadata': new_vol}
finally:
self.common.client_logout()
@utils.synchronized('msa', external=True)
def delete_volume(self, volume):
self.common.client_login()
try:
self.common.delete_volume(volume)
finally:
self.common.client_logout()
@utils.synchronized('msa', external=True)
def initialize_connection(self, volume, connector):
self.common.client_login()
try:
data = {}
data['target_lun'] = self.common.map_volume(volume, connector)
ports = self.common.get_active_fc_target_ports()
data['target_discovered'] = True
data['target_wwn'] = ports
info = {'driver_volume_type': 'fibre_channel',
'data': data}
return info
finally:
self.common.client_logout()
@utils.synchronized('msa', external=True)
def terminate_connection(self, volume, connector, **kwargs):
self.common.client_login()
try:
self.common.unmap_volume(volume, connector)
finally:
self.common.client_logout()
@utils.synchronized('msa', external=True)
def get_volume_stats(self, refresh=False):
if refresh:
self.common.client_login()
try:
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
finally:
if refresh:
self.common.client_logout()
@utils.synchronized('msa', external=True)
def create_export(self, context, volume):
pass
@utils.synchronized('msa', external=True)
def ensure_export(self, context, volume):
pass
@utils.synchronized('msa', external=True)
def remove_export(self, context, volume):
pass
@utils.synchronized('msa', external=True)
def create_snapshot(self, snapshot):
self.common.client_login()
try:
self.common.create_snapshot(snapshot)
finally:
self.common.client_logout()
@utils.synchronized('msa', external=True)
def delete_snapshot(self, snapshot):
self.common.client_login()
try:
self.common.delete_snapshot(snapshot)
finally:
self.common.client_logout()
@utils.synchronized('msa', external=True)
def extend_volume(self, volume, new_size):
self.common.client_login()
try:
self.common.extend_volume(volume, new_size)
finally:
self.common.client_logout()

View File

@ -1539,6 +1539,14 @@
#hplefthand_debug=false
#
# Options defined in cinder.volume.drivers.san.hp.hp_msa_common
#
# The VDisk to use for volume creation. (string value)
#msa_vdisk=OpenStack
#
# Options defined in cinder.volume.drivers.san.san
#