]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Add HP MSA Fiber Channel driver
authorGauvain Pocentek <gauvain.pocentek@objectif-libre.com>
Thu, 13 Feb 2014 20:34:59 +0000 (21:34 +0100)
committerGauvain Pocentek <gauvain.pocentek@objectif-libre.com>
Thu, 20 Feb 2014 12:56:39 +0000 (13:56 +0100)
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

cinder/exception.py
cinder/tests/test_hp_msa.py [new file with mode: 0644]
cinder/volume/drivers/san/hp/hp_msa_client.py [new file with mode: 0644]
cinder/volume/drivers/san/hp/hp_msa_common.py [new file with mode: 0644]
cinder/volume/drivers/san/hp/hp_msa_fc.py [new file with mode: 0644]
etc/cinder/cinder.conf.sample

index e2f1f7b97b4f0d1f68cc53133d203c4bf3a9ddaf..e9d78c55a0b3dbe404b11d17e258bcfa7855a4b6 100644 (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)")
diff --git a/cinder/tests/test_hp_msa.py b/cinder/tests/test_hp_msa.py
new file mode 100644 (file)
index 0000000..662ebcb
--- /dev/null
@@ -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)
diff --git a/cinder/volume/drivers/san/hp/hp_msa_client.py b/cinder/volume/drivers/san/hp/hp_msa_client.py
new file mode 100644 (file)
index 0000000..211046f
--- /dev/null
@@ -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')
diff --git a/cinder/volume/drivers/san/hp/hp_msa_common.py b/cinder/volume/drivers/san/hp/hp_msa_common.py
new file mode 100644 (file)
index 0000000..2223311
--- /dev/null
@@ -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)
diff --git a/cinder/volume/drivers/san/hp/hp_msa_fc.py b/cinder/volume/drivers/san/hp/hp_msa_fc.py
new file mode 100644 (file)
index 0000000..c86650c
--- /dev/null
@@ -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()
index a0890480c46a5a277ade29c8ea5c51fd39f6593b..b7d490124c00ae3d609691613f2aed261f8b3869 100644 (file)
 #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
 #