+++ /dev/null
-# (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 urllib2
-
-import lxml.etree as etree
-import mock
-
-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.assertIsNone(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_with()
-
- @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)
+++ /dev/null
-# 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.
-#
-import hashlib
-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 = hashlib.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')
+++ /dev/null
-# 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 oslo_log import log as logging
-
-from cinder import exception
-from cinder.i18n import _LE
-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):
- """Class that contains common code for MSA drivers.
-
- Version history:
- 0.1 - First release
- 0.2 - Added Logging Markers
-
- """
- VERSION = "0.2"
-
- 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 = (_LE("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:
- msg = _LE("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 = _LE('%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 detached 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 = _LE("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=False):
- 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 = (_LE("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 = _LE("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)
+++ /dev/null
-# 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 oslo_log 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
-from cinder.zonemanager import utils as fczm_utils
-
-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()
-
- @fczm_utils.AddFCZone
- @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()
-
- @fczm_utils.RemoveFCZone
- @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()