import mock
from oslo_utils import units
-import six
from cinder import context
from cinder import exception
from cinder import test
from cinder.tests.unit import fake_hp_lefthand_client as hplefthandclient
from cinder.volume.drivers.san.hp import hp_lefthand_iscsi
-from cinder.volume.drivers.san.hp import hp_lefthand_rest_proxy
from cinder.volume import volume_types
hpexceptions = hplefthandclient.hpexceptions
driver_startup_call_stack = [
mock.call.login('foo1', 'bar2'),
mock.call.getClusterByName('CloudCluster1'),
- mock.call.getCluster(1),
]
-class TestHPLeftHandCLIQISCSIDriver(HPLeftHandBaseDriver, test.TestCase):
-
- def _fake_cliq_run(self, verb, cliq_args, check_exit_code=True):
- """Return fake results for the various methods."""
-
- def create_volume(cliq_args):
- """Create volume CLIQ input for test.
-
- input = "createVolume description="fake description"
- clusterName=Cluster01 volumeName=fakevolume
- thinProvision=0 output=XML size=1GB"
- """
- output = """<gauche version="1.0">
- <response description="Operation succeeded."
- name="CliqSuccess" processingTime="181" result="0"/>
- </gauche>"""
- self.assertEqual(self.volume_name, cliq_args['volumeName'])
- self.assertEqual('1', cliq_args['thinProvision'])
- self.assertEqual('1GB', cliq_args['size'])
- return output, None
-
- def delete_volume(cliq_args):
- """Delete volume CLIQ input for test.
-
- input = "deleteVolume volumeName=fakevolume prompt=false
- output=XML"
- """
- output = """<gauche version="1.0">
- <response description="Operation succeeded."
- name="CliqSuccess" processingTime="164" result="0"/>
- </gauche>"""
- self.assertEqual(self.volume_name, cliq_args['volumeName'])
- self.assertEqual('false', cliq_args['prompt'])
- return output, None
-
- def extend_volume(cliq_args):
- """Extend volume CLIQ input for test.
-
- input = "modifyVolume description="fake description"
- volumeName=fakevolume
- output=XML size=2GB"
- """
- output = """<gauche version="1.0">
- <response description="Operation succeeded."
- name="CliqSuccess" processingTime="181" result="0"/>
- </gauche>"""
- self.assertEqual(self.volume_name, cliq_args['volumeName'])
- self.assertEqual('2GB', cliq_args['size'])
- return output, None
-
- def assign_volume(cliq_args):
- """Assign volume CLIQ input for test.
-
- input = "assignVolumeToServer volumeName=fakevolume
- serverName=fakehost
- output=XML"
- """
- output = """<gauche version="1.0">
- <response description="Operation succeeded."
- name="CliqSuccess" processingTime="174" result="0"/>
- </gauche>"""
- self.assertEqual(self.volume_name, cliq_args['volumeName'])
- self.assertEqual(self.connector['host'],
- cliq_args['serverName'])
- return output, None
-
- def unassign_volume(cliq_args):
- """Unassign volume CLIQ input for test.
-
- input = "unassignVolumeToServer volumeName=fakevolume
- serverName=fakehost output=XML
- """
- output = """<gauche version="1.0">
- <response description="Operation succeeded."
- name="CliqSuccess" processingTime="205" result="0"/>
- </gauche>"""
- self.assertEqual(self.volume_name, cliq_args['volumeName'])
- self.assertEqual(self.connector['host'],
- cliq_args['serverName'])
- return output, None
-
- def create_snapshot(cliq_args):
- """Create snapshot CLIQ input for test.
-
- input = "createSnapshot description="fake description"
- snapshotName=fakesnapshot
- volumeName=fakevolume
- output=XML"
- """
- output = """<gauche version="1.0">
- <response description="Operation succeeded."
- name="CliqSuccess" processingTime="181" result="0"/>
- </gauche>"""
- self.assertEqual(self.snapshot_name, cliq_args['snapshotName'])
- self.assertEqual(self.volume_name, cliq_args['volumeName'])
- return output, None
-
- def delete_snapshot(cliq_args):
- """Delete shapshot CLIQ input for test.
-
- input = "deleteSnapshot snapshotName=fakesnapshot prompt=false
- output=XML"
- """
- output = """<gauche version="1.0">
- <response description="Operation succeeded."
- name="CliqSuccess" processingTime="164" result="0"/>
- </gauche>"""
- self.assertEqual(self.snapshot_name, cliq_args['snapshotName'])
- self.assertEqual('false', cliq_args['prompt'])
- return output, None
-
- def create_volume_from_snapshot(cliq_args):
- """Create volume from snapshot CLIQ input for test.
-
- input = "cloneSnapshot description="fake description"
- snapshotName=fakesnapshot
- volumeName=fakevolume
- output=XML"
- """
- output = """<gauche version="1.0">
- <response description="Operation succeeded."
- name="CliqSuccess" processingTime="181" result="0"/>
- </gauche>"""
- self.assertEqual(self.snapshot_name, cliq_args['snapshotName'])
- self.assertEqual(self.volume_name, cliq_args['volumeName'])
- return output, None
-
- def get_cluster_info(cliq_args):
- """Get cluster info CLIQ input for test.
-
- input = "getClusterInfo clusterName=Cluster01 searchDepth=1
- verbose=0 output=XML"
- """
- output = """<gauche version="1.0">
- <response description="Operation succeeded." name="CliqSuccess"
- processingTime="1164" result="0">
- <cluster blockSize="1024" description=""
- maxVolumeSizeReplication1="622957690"
- maxVolumeSizeReplication2="311480287"
- minVolumeSize="262144" name="Cluster01"
- pageSize="262144" spaceTotal="633697992"
- storageNodeCount="2" unprovisionedSpace="622960574"
- useVip="true">
- <nsm ipAddress="10.0.1.7" name="111-vsa"/>
- <nsm ipAddress="10.0.1.8" name="112-vsa"/>
- <vip ipAddress="10.0.1.6" subnetMask="255.255.255.0"/>
- </cluster></response></gauche>"""
- return output, None
-
- def get_volume_info(cliq_args):
- """Get volume info CLIQ input for test.
-
- input = "getVolumeInfo volumeName=fakevolume output=XML"
- """
- output = """<gauche version="1.0">
- <response description="Operation succeeded." name="CliqSuccess"
- processingTime="87" result="0">
- <volume autogrowPages="4" availability="online"
- blockSize="1024" bytesWritten="0" checkSum="false"
- clusterName="Cluster01" created="2011-02-08T19:56:53Z"
- deleting="false" description="" groupName="Group01"
- initialQuota="536870912" isPrimary="true"
- iscsiIqn="iqn.2003-10.com.lefthandnetworks:group01:25366:fakev"
- maxSize="6865387257856" md5="9fa5c8b2cca54b2948a63d833097e1ca"
- minReplication="1" name="vol-b" parity="0" replication="2"
- reserveQuota="536870912" scratchQuota="4194304"
- serialNumber="9fa5c8b2cca54b2948a63d8"
- size="1073741824" stridePages="32" thinProvision="true">
- <status description="OK" value="2"/>
- <permission access="rw" authGroup="api-1"
- chapName="chapusername" chapRequired="true"
- id="25369" initiatorSecret="" iqn=""
- iscsiEnabled="true" loadBalance="true"
- targetSecret="supersecret"/>
- </volume></response></gauche>"""
- return output, None
-
- def get_snapshot_info(cliq_args):
- """Get snapshot info CLIQ input for test.
-
- input = "getSnapshotInfo snapshotName=fakesnapshot output=XML"
- """
- output = """<gauche version="1.0">
- <response description="Operation succeeded." name="CliqSuccess"
- processingTime="87" result="0">
- <snapshot applicationManaged="false" autogrowPages="32768"
- automatic="false" availability="online" bytesWritten="0"
- clusterName="CloudCluster1" created="2013-08-26T07:03:44Z"
- deleting="false" description="" groupName="CloudGroup1"
- id="730" initialQuota="536870912" isPrimary="true"
- iscsiIqn="iqn.2003-10.com.lefthandnetworks:cloudgroup1:73"
- md5="a64b4f850539c07fb5ce3cee5db1fcce" minReplication="1"
- name="snapshot-7849288e-e5e8-42cb-9687-9af5355d674b"
- replication="2" reserveQuota="536870912" scheduleId="0"
- scratchQuota="4194304" scratchWritten="0"
- serialNumber="a64b4f850539c07fb5ce3cee5db1fcce"
- size="2147483648" stridePages="32"
- volumeSerial="a64b4f850539c07fb5ce3cee5db1fcce">
- <status description="OK" value="2"/>
- <permission access="rw"
- authGroup="api-34281B815713B78-(trimmed)51ADD4B7030853AA7"
- chapName="chapusername" chapRequired="true" id="25369"
- initiatorSecret="" iqn="" iscsiEnabled="true"
- loadBalance="true" targetSecret="supersecret"/>
- </snapshot></response></gauche>"""
- return output, None
-
- def get_server_info(cliq_args):
- """Get server info CLIQ input for test.
-
- input = "getServerInfo serverName=fakeName"
- """
- output = """<gauche version="1.0"><response result="0"/>
- </gauche>"""
- return output, None
-
- def create_server(cliq_args):
- """Create server CLIQ input for test.
-
- input = "createServer serverName=fakeName initiator=something"
- """
- output = """<gauche version="1.0"><response result="0"/>
- </gauche>"""
- return output, None
-
- def test_error(cliq_args):
- output = """<gauche version="1.0">
- <response description="Volume '134234' not found."
- name="CliqVolumeNotFound" processingTime="1083"
- result="8000100c"/>
- </gauche>"""
- return output, None
-
- def test_paramiko_1_13_0(cliq_args):
-
- # paramiko 1.13.0 now returns unicode
- output = six.text_type(
- '<?xml version="1.0" encoding="UTF-8" standalone="no" ?>\n'
- '<gauche version="1.0">\n\n <response description="Operation'
- ' succeeded." name="CliqSuccess" processingTime="423" '
- 'result="0">\n <cluster adaptiveOptimization="false" '
- 'blockSize="1024" description="" maxVolumeSizeReplication1='
- '"114594676736" minVolumeSize="262144" name="clusterdemo" '
- 'pageSize="262144" spaceTotal="118889644032" storageNodeCount='
- '"1" unprovisionedSpace="114594676736" useVip="true">\n'
- ' <nsm ipAddress="10.10.29.102" name="lefdemo1"/>\n'
- ' <vip ipAddress="10.10.22.87" subnetMask='
- '"255.255.224.0"/>\n </cluster>\n </response>\n\n'
- '</gauche>\n ')
- return output, None
-
- def test_paramiko_1_10_0(cliq_args):
-
- # paramiko 1.10.0 returns python default encoding.
- output = (
- '<?xml version="1.0" encoding="UTF-8" standalone="no" ?>\n'
- '<gauche version="1.0">\n\n <response description="Operation'
- ' succeeded." name="CliqSuccess" processingTime="423" '
- 'result="0">\n <cluster adaptiveOptimization="false" '
- 'blockSize="1024" description="" maxVolumeSizeReplication1='
- '"114594676736" minVolumeSize="262144" name="clusterdemo" '
- 'pageSize="262144" spaceTotal="118889644032" storageNodeCount='
- '"1" unprovisionedSpace="114594676736" useVip="true">\n'
- ' <nsm ipAddress="10.10.29.102" name="lefdemo1"/>\n'
- ' <vip ipAddress="10.10.22.87" subnetMask='
- '"255.255.224.0"/>\n </cluster>\n </response>\n\n'
- '</gauche>\n ')
- return output, None
-
- self.assertEqual('XML', cliq_args['output'])
- try:
- verbs = {'createVolume': create_volume,
- 'deleteVolume': delete_volume,
- 'modifyVolume': extend_volume,
- 'assignVolumeToServer': assign_volume,
- 'unassignVolumeToServer': unassign_volume,
- 'createSnapshot': create_snapshot,
- 'deleteSnapshot': delete_snapshot,
- 'cloneSnapshot': create_volume_from_snapshot,
- 'getClusterInfo': get_cluster_info,
- 'getVolumeInfo': get_volume_info,
- 'getSnapshotInfo': get_snapshot_info,
- 'getServerInfo': get_server_info,
- 'createServer': create_server,
- 'testError': test_error,
- 'testParamiko_1.10.1': test_paramiko_1_10_0,
- 'testParamiko_1.13.1': test_paramiko_1_13_0}
- except KeyError:
- raise NotImplementedError()
-
- return verbs[verb](cliq_args)
-
- def setUp(self):
- super(TestHPLeftHandCLIQISCSIDriver, self).setUp()
-
- self.properties = {
- 'target_discovered': True,
- 'target_portal': '10.0.1.6:3260',
- 'target_iqn':
- 'iqn.2003-10.com.lefthandnetworks:group01:25366:fakev',
- 'volume_id': self.volume_id}
-
- def default_mock_conf(self):
-
- mock_conf = mock.Mock()
- mock_conf.san_ip = '10.10.10.10'
- mock_conf.san_login = 'foo'
- mock_conf.san_password = 'bar'
- mock_conf.san_ssh_port = 16022
- mock_conf.san_clustername = 'CloudCluster1'
- mock_conf.hplefthand_api_url = None
- return mock_conf
-
- def setup_driver(self, config=None):
-
- if config is None:
- config = self.default_mock_conf()
-
- self.driver = hp_lefthand_iscsi.HPLeftHandISCSIDriver(
- configuration=config)
- self.driver.do_setup(None)
-
- self.driver.proxy._cliq_run = mock.Mock(
- side_effect=self._fake_cliq_run)
- return self.driver.proxy._cliq_run
-
- def test_create_volume(self):
-
- # set up driver with default config
- mock_cliq_run = self.setup_driver()
-
- volume = {'name': self.volume_name, 'size': 1}
- model_update = self.driver.create_volume(volume)
- expected_iqn = "iqn.2003-10.com.lefthandnetworks:group01:25366:fakev 0"
- expected_location = "10.0.1.6:3260,1 %s" % expected_iqn
- self.assertEqual(expected_location, model_update['provider_location'])
-
- expected = [
- mock.call(
- 'createVolume', {
- 'clusterName': 'CloudCluster1',
- 'volumeName': 'fakevolume',
- 'thinProvision': '1',
- 'output': 'XML',
- 'size': '1GB'},
- True),
- mock.call(
- 'getVolumeInfo', {
- 'volumeName': 'fakevolume',
- 'output': 'XML'},
- True),
- mock.call(
- 'getClusterInfo', {
- 'clusterName': 'Cluster01',
- 'searchDepth': '1',
- 'verbose': '0',
- 'output': 'XML'},
- True)]
-
- # validate call chain
- mock_cliq_run.assert_has_calls(expected)
-
- def test_delete_volume(self):
-
- # set up driver with default config
- mock_cliq_run = self.setup_driver()
-
- volume = {'name': self.volume_name}
- self.driver.delete_volume(volume)
-
- expected = [
- mock.call(
- 'getVolumeInfo', {
- 'volumeName': 'fakevolume',
- 'output': 'XML'},
- True),
- mock.call(
- 'deleteVolume', {
- 'volumeName': 'fakevolume',
- 'prompt': 'false',
- 'output': 'XML'},
- True)]
-
- # validate call chain
- mock_cliq_run.assert_has_calls(expected)
-
- def test_extend_volume(self):
-
- # set up driver with default config
- mock_cliq_run = self.setup_driver()
-
- volume = {'name': self.volume_name}
- self.driver.extend_volume(volume, 2)
-
- expected = [
- mock.call(
- 'modifyVolume', {
- 'volumeName': 'fakevolume',
- 'output': 'XML',
- 'size': '2GB'},
- True)]
-
- # validate call chain
- mock_cliq_run.assert_has_calls(expected)
-
- def test_initialize_connection(self):
-
- # set up driver with default config
- mock_cliq_run = self.setup_driver()
-
- self.driver.proxy._get_iscsi_properties = mock.Mock(
- return_value=self.properties)
- volume = {'name': self.volume_name}
- result = self.driver.initialize_connection(volume,
- self.connector)
- self.assertEqual('iscsi', result['driver_volume_type'])
- self.assertDictMatch(self.properties, result['data'])
-
- expected = [
- mock.call(
- 'getServerInfo', {
- 'output': 'XML',
- 'serverName': 'fakehost'},
- False),
- mock.call(
- 'assignVolumeToServer', {
- 'volumeName': 'fakevolume',
- 'serverName': 'fakehost',
- 'output': 'XML'},
- True)]
-
- # validate call chain
- mock_cliq_run.assert_has_calls(expected)
-
- def test_terminate_connection(self):
-
- # set up driver with default config
- mock_cliq_run = self.setup_driver()
-
- volume = {'name': self.volume_name}
- self.driver.terminate_connection(volume, self.connector)
-
- expected = [
- mock.call(
- 'unassignVolumeToServer', {
- 'volumeName': 'fakevolume',
- 'serverName': 'fakehost',
- 'output': 'XML'},
- True)]
-
- # validate call chain
- mock_cliq_run.assert_has_calls(expected)
-
- def test_create_snapshot(self):
-
- # set up driver with default config
- mock_cliq_run = self.setup_driver()
-
- snapshot = {'name': self.snapshot_name,
- 'volume_name': self.volume_name}
- self.driver.create_snapshot(snapshot)
-
- expected = [
- mock.call(
- 'createSnapshot', {
- 'snapshotName': 'fakeshapshot',
- 'output': 'XML',
- 'inheritAccess': 1,
- 'volumeName': 'fakevolume'},
- True)]
-
- # validate call chain
- mock_cliq_run.assert_has_calls(expected)
-
- def test_delete_snapshot(self):
-
- # set up driver with default config
- mock_cliq_run = self.setup_driver()
-
- snapshot = {'name': self.snapshot_name}
- self.driver.delete_snapshot(snapshot)
-
- expected = [
- mock.call(
- 'getSnapshotInfo', {
- 'snapshotName': 'fakeshapshot',
- 'output': 'XML'},
- True),
- mock.call(
- 'deleteSnapshot', {
- 'snapshotName': 'fakeshapshot',
- 'prompt': 'false',
- 'output': 'XML'},
- True)]
-
- # validate call chain
- mock_cliq_run.assert_has_calls(expected)
-
- def test_create_volume_from_snapshot(self):
-
- # set up driver with default config
- mock_cliq_run = self.setup_driver()
-
- volume = {'name': self.volume_name}
- snapshot = {'name': self.snapshot_name}
- model_update = self.driver.create_volume_from_snapshot(volume,
- snapshot)
- expected_iqn = "iqn.2003-10.com.lefthandnetworks:group01:25366:fakev 0"
- expected_location = "10.0.1.6:3260,1 %s" % expected_iqn
- self.assertEqual(expected_location, model_update['provider_location'])
-
- expected = [
- mock.call(
- 'cloneSnapshot', {
- 'snapshotName': 'fakeshapshot',
- 'output': 'XML',
- 'volumeName': 'fakevolume'},
- True),
- mock.call(
- 'getVolumeInfo', {
- 'volumeName': 'fakevolume',
- 'output': 'XML'},
- True),
- mock.call(
- 'getClusterInfo', {
- 'clusterName': 'Cluster01',
- 'searchDepth': '1',
- 'verbose': '0',
- 'output': 'XML'},
- True)]
-
- # validate call chain
- mock_cliq_run.assert_has_calls(expected)
-
- def test_get_volume_stats(self):
-
- # set up driver with default config
- mock_cliq_run = self.setup_driver()
- volume_stats = self.driver.get_volume_stats(True)
-
- self.assertEqual('Hewlett-Packard', volume_stats['vendor_name'])
- self.assertEqual('iSCSI', volume_stats['storage_protocol'])
-
- expected = [
- mock.call('getClusterInfo', {
- 'searchDepth': 1,
- 'clusterName': 'CloudCluster1',
- 'output': 'XML'}, True)]
-
- # validate call chain
- mock_cliq_run.assert_has_calls(expected)
-
- def test_cliq_run_xml_paramiko_1_13_0(self):
-
- # set up driver with default config
- self.setup_driver()
- xml = self.driver.proxy._cliq_run_xml('testParamiko_1.13.1', {})
- self.assertIsNotNone(xml)
-
- def test_cliq_run_xml_paramiko_1_10_0(self):
-
- # set up driver with default config
- self.setup_driver()
- xml = self.driver.proxy._cliq_run_xml('testParamiko_1.10.1', {})
- self.assertIsNotNone(xml)
-
-
-class TestHPLeftHandRESTISCSIDriver(HPLeftHandBaseDriver, test.TestCase):
+class TestHPLeftHandISCSIDriver(HPLeftHandBaseDriver, test.TestCase):
CONSIS_GROUP_ID = '3470cc4c-63b3-4c7a-8120-8a0693b45838'
CGSNAPSHOT_ID = '5351d914-6c90-43e7-9a8e-7e84610927da'
'id': CGSNAPSHOT_ID,
'readOnly': False}
- driver_startup_call_stack = [
- mock.call.login('foo1', 'bar2'),
- mock.call.getClusterByName('CloudCluster1'),
- mock.call.getCluster(1),
- mock.call.getVolumes(
- cluster='CloudCluster1',
- fields=['members[id]', 'members[clusterName]', 'members[size]']),
- ]
-
def default_mock_conf(self):
mock_conf = mock.Mock()
@mock.patch('hplefthandclient.client.HPLeftHandClient', spec=True)
def setup_driver(self, _mock_client, config=None):
-
if config is None:
config = self.default_mock_conf()
mock_client.createVolume.return_value = {
'iscsiIqn': self.connector['initiator']}
- with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy,
+ with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver,
'_create_client') as mock_do_setup:
mock_do_setup.return_value = mock_client
'iscsiIqn': self.connector['initiator']}
mock_client.getVolumes.return_value = {'total': 1, 'members': []}
- with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy,
+ with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver,
'_create_client') as mock_do_setup:
mock_do_setup.return_value = mock_client
mock_client.getVolumeByName.return_value = {'id': self.volume_id}
mock_client.getVolumes.return_value = {'total': 1, 'members': []}
- with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy,
+ with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver,
'_create_client') as mock_do_setup:
mock_do_setup.return_value = mock_client
mock_client.getVolumeByName.return_value = {'id': self.volume_id}
mock_client.getVolumes.return_value = {'total': 1, 'members': []}
- with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy,
+ with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver,
'_create_client') as mock_do_setup:
mock_do_setup.return_value = mock_client
}
mock_client.getVolumes.return_value = {'total': 1, 'members': []}
- with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy,
+ with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver,
'_create_client') as mock_do_setup:
mock_do_setup.return_value = mock_client
}
mock_client.getVolumes.return_value = {'total': 1, 'members': []}
- with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy,
+ with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver,
'_create_client') as mock_do_setup:
mock_do_setup.return_value = mock_client
}
mock_client.getVolumes.return_value = {'total': 1, 'members': []}
- with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy,
+ with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver,
'_create_client') as mock_do_setup:
mock_do_setup.return_value = mock_client
mock_client.findServerVolumes.return_value = [{'id': self.volume_id}]
mock_client.getVolumes.return_value = {'total': 1, 'members': []}
- with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy,
+ with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver,
'_create_client') as mock_do_setup:
mock_do_setup.return_value = mock_client
{'id': 99999}]
mock_client.getVolumes.return_value = {'total': 1, 'members': []}
- with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy,
+ with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver,
'_create_client') as mock_do_setup:
mock_do_setup.return_value = mock_client
mock_client.getVolumeByName.return_value = {'id': self.volume_id}
mock_client.getVolumes.return_value = {'total': 1, 'members': []}
- with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy,
+ with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver,
'_create_client') as mock_do_setup:
mock_do_setup.return_value = mock_client
mock_client.getSnapshotByName.return_value = {'id': self.snapshot_id}
mock_client.getVolumes.return_value = {'total': 1, 'members': []}
- with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy,
+ with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver,
'_create_client') as mock_do_setup:
mock_do_setup.return_value = mock_client
'iscsiIqn': self.connector['initiator']}
mock_client.getVolumes.return_value = {'total': 1, 'members': []}
- with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy,
+ with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver,
'_create_client') as mock_do_setup:
mock_do_setup.return_value = mock_client
'iscsiIqn': self.connector['initiator']}
mock_client.getVolumes.return_value = {'total': 1, 'members': []}
- with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy,
+ with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver,
'_create_client') as mock_do_setup:
mock_do_setup.return_value = mock_client
volume_with_vt['volume_type_id'] = self.volume_type_id
# get the extra specs of interest from this volume's volume type
- volume_extra_specs = self.driver.proxy._get_volume_extra_specs(
+ volume_extra_specs = self.driver._get_volume_extra_specs(
volume_with_vt)
- extra_specs = self.driver.proxy._get_lh_extra_specs(
+ extra_specs = self.driver._get_lh_extra_specs(
volume_extra_specs,
- hp_lefthand_rest_proxy.extra_specs_key_map.keys())
+ hp_lefthand_iscsi.extra_specs_key_map.keys())
# map the extra specs key/value pairs to key/value pairs
# used as optional configuration values by the LeftHand backend
- optional = self.driver.proxy._map_extra_specs(extra_specs)
+ optional = self.driver._map_extra_specs(extra_specs)
self.assertDictMatch({'isThinProvisioned': False}, optional)
'hplh:ao': 'true'}}
# get the extra specs of interest from this volume's volume type
- volume_extra_specs = self.driver.proxy._get_volume_extra_specs(
+ volume_extra_specs = self.driver._get_volume_extra_specs(
volume_with_vt)
- extra_specs = self.driver.proxy._get_lh_extra_specs(
+ extra_specs = self.driver._get_lh_extra_specs(
volume_extra_specs,
- hp_lefthand_rest_proxy.extra_specs_key_map.keys())
+ hp_lefthand_iscsi.extra_specs_key_map.keys())
# map the extra specs key/value pairs to key/value pairs
# used as optional configuration values by the LeftHand backend
- optional = self.driver.proxy._map_extra_specs(extra_specs)
+ optional = self.driver._map_extra_specs(extra_specs)
# {'hplh:ao': 'true'} should map to
# {'isAdaptiveOptimizationEnabled': True}
volume['host'] = host
new_type = volume_types.get_volume_type(ctxt, new_type_ref['id'])
- with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy,
+ with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver,
'_create_client') as mock_do_setup:
mock_do_setup.return_value = mock_client
volume['host'] = host
new_type = volume_types.get_volume_type(ctxt, new_type_ref['id'])
- with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy,
+ with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver,
'_create_client') as mock_do_setup:
mock_do_setup.return_value = mock_client
volume['host'] = host
new_type = volume_types.get_volume_type(ctxt, new_type_ref['id'])
- with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy,
+ with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver,
'_create_client') as mock_do_setup:
mock_do_setup.return_value = mock_client
volume['host'] = host
new_type = volume_types.get_volume_type(ctxt, new_type_ref['id'])
- with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy,
+ with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver,
'_create_client') as mock_do_setup:
mock_do_setup.return_value = mock_client
host = {'host': self.serverName, 'capabilities': {}}
- with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy,
+ with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver,
'_create_client') as mock_do_setup:
mock_do_setup.return_value = mock_client
mock_client.getVolumeByName.return_value = {'id': self.volume_id}
mock_client.getVolumes.return_value = {'total': 1, 'members': []}
- location = (self.driver.proxy.DRIVER_LOCATION % {
+ location = (self.driver.DRIVER_LOCATION % {
'cluster': 'New_CloudCluster',
'vip': '10.10.10.111'})
'host': self.serverName,
'capabilities': {'location_info': location}}
- with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy,
+ with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver,
'_create_client') as mock_do_setup:
mock_do_setup.return_value = mock_client
'resource': None}}
mock_client.getVolumes.return_value = {'total': 1, 'members': []}
- location = (self.driver.proxy.DRIVER_LOCATION % {
+ location = (self.driver.DRIVER_LOCATION % {
'cluster': 'New_CloudCluster',
'vip': '10.10.10.111'})
'host': self.serverName,
'capabilities': {'location_info': location}}
- with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy,
+ with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver,
'_create_client') as mock_do_setup:
mock_do_setup.return_value = mock_client
'resource': 'snapfoo'}}
mock_client.getVolumes.return_value = {'total': 1, 'members': []}
- location = (self.driver.proxy.DRIVER_LOCATION % {
+ location = (self.driver.DRIVER_LOCATION % {
'cluster': 'New_CloudCluster',
'vip': '10.10.10.111'})
'host': self.serverName,
'capabilities': {'location_info': location}}
- with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy,
+ with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver,
'_create_client') as mock_do_setup:
mock_do_setup.return_value = mock_client
'_name_id': clone_id,
'provider_location': provider_location}
original_volume_status = 'available'
- with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy,
+ with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver,
'_create_client') as mock_do_setup:
mock_do_setup.return_value = mock_client
actual_update = self.driver.update_migrated_volume(
'provider_location': provider_location}
original_volume_status = 'in-use'
- with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy,
+ with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver,
'_create_client') as mock_do_setup:
mock_do_setup.return_value = mock_client
actual_update = self.driver.update_migrated_volume(
'iscsiIqn': self.connector['initiator']}
mock_client.getVolumes.return_value = {'total': 1, 'members': []}
- with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy,
+ with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver,
'_create_client') as mock_do_setup:
mock_do_setup.return_value = mock_client
'iscsiIqn': self.connector['initiator']}
mock_client.getVolumes.return_value = {'total': 1, 'members': []}
- with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy,
+ with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver,
'_create_client') as mock_do_setup:
mock_do_setup.return_value = mock_client
mock_client.assert_has_calls(expected)
- def test__get_existing_volume_ref_name(self):
+ def test_get_existing_volume_ref_name(self):
self.setup_driver()
existing_ref = {'source-name': self.volume_name}
- result = self.driver.proxy._get_existing_volume_ref_name(
+ result = self.driver._get_existing_volume_ref_name(
existing_ref)
self.assertEqual(self.volume_name, result)
existing_ref = {'bad-key': 'foo'}
self.assertRaises(
exception.ManageExistingInvalidReference,
- self.driver.proxy._get_existing_volume_ref_name,
+ self.driver._get_existing_volume_ref_name,
existing_ref)
def test_manage_existing(self):
mock_client = self.setup_driver()
- self.driver.proxy.api_version = "1.1"
+ self.driver.api_version = "1.1"
volume = {'display_name': 'Foo Volume',
'volume_type': None,
'volume_type_id': None,
'id': '12345'}
- with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy,
+ with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver,
'_create_client') as mock_do_setup:
mock_do_setup.return_value = mock_client
mock_client.getVolumeByName.return_value = {'id': self.volume_id}
'hplh:data_pl': 'r-0',
'volume_type': self.volume_type}}
- self.driver.proxy.api_version = "1.1"
+ self.driver.api_version = "1.1"
volume = {'display_name': 'Foo Volume',
'host': 'stack@lefthand#lefthand',
'volume_type_id': 'bcfa9fa4-54a0-4340-a3d8-bfcf19aea65e',
'id': '12345'}
- with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy,
+ with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver,
'_create_client') as mock_do_setup:
mock_do_setup.return_value = mock_client
mock_client.getVolumeByName.return_value = {'id': self.volume_id}
'hplh:data_pl': 'r-0',
'volume_type': self.volume_type}}
- self.driver.proxy.retype = mock.Mock(
+ self.driver.retype = mock.Mock(
side_effect=exception.VolumeNotFound(volume_id="fake"))
- self.driver.proxy.api_version = "1.1"
+ self.driver.api_version = "1.1"
volume = {'display_name': 'Foo Volume',
'host': 'stack@lefthand#lefthand',
'volume_type_id': 'bcfa9fa4-54a0-4340-a3d8-bfcf19aea65e',
'id': '12345'}
- with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy,
+ with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver,
'_create_client') as mock_do_setup:
mock_do_setup.return_value = mock_client
mock_client.getVolumeByName.return_value = {'id': self.volume_id}
def test_manage_existing_volume_type_exception(self):
mock_client = self.setup_driver()
- self.driver.proxy.api_version = "1.1"
+ self.driver.api_version = "1.1"
volume = {'display_name': 'Foo Volume',
'volume_type': 'gold',
'volume_type_id': 'bcfa9fa4-54a0-4340-a3d8-bfcf19aea65e',
'id': '12345'}
- with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy,
+ with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver,
'_create_client') as mock_do_setup:
mock_do_setup.return_value = mock_client
mock_client.getVolumeByName.return_value = {'id': self.volume_id}
mock_client = self.setup_driver()
mock_client.getVolumeByName.return_value = {'size': 2147483648}
- self.driver.proxy.api_version = "1.1"
+ self.driver.api_version = "1.1"
- with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy,
+ with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver,
'_create_client') as mock_do_setup:
mock_do_setup.return_value = mock_client
mock_client.getVolumes.return_value = {
mock_client = self.setup_driver()
mock_client.getVolumeByName.return_value = {'size': 2147483648}
- self.driver.proxy.api_version = "1.1"
+ self.driver.api_version = "1.1"
- with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy,
+ with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver,
'_create_client') as mock_do_setup:
mock_do_setup.return_value = mock_client
mock_client.getVolumeByName.side_effect = (
hpexceptions.HTTPNotFound('fake'))
- self.driver.proxy.api_version = "1.1"
+ self.driver.api_version = "1.1"
- with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy,
+ with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver,
'_create_client') as mock_do_setup:
mock_do_setup.return_value = mock_client
mock_client.getVolumes.return_value = {
}]
}
- self.driver.proxy.api_version = "1.1"
+ self.driver.api_version = "1.1"
- with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy,
+ with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver,
'_create_client') as mock_do_setup:
mock_do_setup.return_value = mock_client
self.driver.unmanage(self.volume)
def test_api_version(self):
self.setup_driver()
- self.driver.proxy.api_version = "1.1"
- self.driver.proxy._check_api_version()
+ self.driver.api_version = "1.1"
+ self.driver._check_api_version()
- self.driver.proxy.api_version = "1.0"
+ self.driver.api_version = "1.0"
self.assertRaises(exception.InvalidInput,
- self.driver.proxy._check_api_version)
+ self.driver._check_api_version)
def test_get_volume_stats(self):
}]
}
- with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy,
+ with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver,
'_create_client') as mock_do_setup:
mock_do_setup.return_value = mock_client
# set up driver with default config
mock_client = self.setup_driver()
- with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy,
+ with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver,
'_create_client') as mock_do_setup:
mock_do_setup.return_value = mock_client
expected_volumes = [mock_volume]
self.driver.db.volume_get_all_by_group.return_value = expected_volumes
- with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy,
+ with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver,
'_create_client') as mock_do_setup:
mock_do_setup.return_value = mock_client
mock_client.createVolume.return_value = {
'iscsiIqn': self.connector['initiator']}
- with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy,
+ with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver,
'_create_client') as mock_do_setup:
mock_do_setup.return_value = mock_client
mock_client.createVolume.return_value = {
'iscsiIqn': self.connector['initiator']}
- with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy,
+ with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver,
'_create_client') as mock_do_setup:
mock_do_setup.return_value = mock_client
expected_snaps = [mock_snap]
mock_snap_list.return_value = expected_snaps
- with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy,
+ with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver,
'_create_client') as mock_do_setup:
mock_do_setup.return_value = mock_client
expected_snaps = [mock_snap]
mock_snap_list.return_value = expected_snaps
- with mock.patch.object(hp_lefthand_rest_proxy.HPLeftHandRESTProxy,
+ with mock.patch.object(hp_lefthand_iscsi.HPLeftHandISCSIDriver,
'_create_client') as mock_do_setup:
mock_do_setup.return_value = mock_client
+++ /dev/null
-# (c) Copyright 2014 Hewlett-Packard Development Company, L.P.
-# All Rights Reserved.
-#
-# 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.
-#
-"""
-HP LeftHand SAN ISCSI Driver.
-
-The driver communicates to the backend aka Cliq via SSH to perform all the
-operations on the SAN.
-"""
-
-from lxml import etree
-from oslo_concurrency import processutils
-from oslo_log import log as logging
-from oslo_utils import units
-
-from cinder import exception
-from cinder.i18n import _, _LE, _LW
-from cinder.volume.drivers.san import san
-
-
-LOG = logging.getLogger(__name__)
-
-
-class HPLeftHandCLIQProxy(san.SanISCSIDriver):
- """Executes commands relating to HP/LeftHand SAN ISCSI volumes.
-
- We use the CLIQ interface, over SSH.
-
- Rough overview of CLIQ commands used:
-
- :createVolume: (creates the volume)
-
- :deleteVolume: (deletes the volume)
-
- :modifyVolume: (extends the volume)
-
- :createSnapshot: (creates the snapshot)
-
- :deleteSnapshot: (deletes the snapshot)
-
- :cloneSnapshot: (creates the volume from a snapshot)
-
- :getVolumeInfo: (to discover the IQN etc)
-
- :getSnapshotInfo: (to discover the IQN etc)
-
- :getClusterInfo: (to discover the iSCSI target IP address)
-
- The 'trick' here is that the HP SAN enforces security by default, so
- normally a volume mount would need both to configure the SAN in the volume
- layer and do the mount on the compute layer. Multi-layer operations are
- not catered for at the moment in the cinder architecture, so instead we
- share the volume using CHAP at volume creation time. Then the mount need
- only use those CHAP credentials, so can take place exclusively in the
- compute layer.
-
- Version history:
- 1.0.0 - Initial driver
- 1.1.0 - Added create/delete snapshot, extend volume, create volume
- from snapshot support.
- 1.2.0 - Ported into the new HP LeftHand driver.
- 1.2.1 - Fixed bug #1279897, HP LeftHand CLIQ proxy may return incorrect
- capacity values.
- 1.2.2 - Fixed driver with Paramiko 1.13.0, bug #1298608.
- 1.2.3 - Added update_migrated_volume #1493546
- """
-
- VERSION = "1.2.3"
-
- device_stats = {}
-
- def __init__(self, *args, **kwargs):
- super(HPLeftHandCLIQProxy, self).__init__(*args, **kwargs)
- self.cluster_vip = None
- LOG.warning(_LW('The HPLeftHandISCSIDriver CLIQ driver has been '
- 'DEPRECATED as of the 2015.2 release. This driver '
- 'will be removed in the 2016.1 release. Please use '
- 'the HPLeftHandISCSIDriver REST based driver '
- 'instead.'))
-
- def do_setup(self, context):
- pass
-
- def check_for_setup_error(self):
- pass
-
- def get_version_string(self):
- return (_('CLIQ %(proxy_ver)s') % {'proxy_ver': self.VERSION})
-
- def _cliq_run(self, verb, cliq_args, check_exit_code=True):
- """Runs a CLIQ command over SSH, without doing any result parsing."""
- cmd_list = [verb]
- for k, v in cliq_args.items():
- cmd_list.append("%s=%s" % (k, v))
-
- return self._run_ssh(cmd_list, check_exit_code)
-
- def _cliq_run_xml(self, verb, cliq_args, check_cliq_result=True):
- """Runs a CLIQ command over SSH, parsing and checking the output."""
- cliq_args['output'] = 'XML'
- (out, _err) = self._cliq_run(verb, cliq_args, check_cliq_result)
-
- LOG.debug("CLIQ command returned %s", out)
-
- result_xml = etree.fromstring(out.encode('utf8'))
- if check_cliq_result:
- response_node = result_xml.find("response")
- if response_node is None:
- msg = (_("Malformed response to CLIQ command "
- "%(verb)s %(cliq_args)s. Result=%(out)s") %
- {'verb': verb, 'cliq_args': cliq_args, 'out': out})
- raise exception.VolumeBackendAPIException(data=msg)
-
- result_code = response_node.attrib.get("result")
-
- if result_code != "0":
- msg = (_("Error running CLIQ command %(verb)s %(cliq_args)s. "
- " Result=%(out)s") %
- {'verb': verb, 'cliq_args': cliq_args, 'out': out})
- raise exception.VolumeBackendAPIException(data=msg)
-
- return result_xml
-
- def _cliq_get_cluster_info(self, cluster_name):
- """Queries for info about the cluster (including IP)."""
- cliq_args = {}
- cliq_args['clusterName'] = cluster_name
- cliq_args['searchDepth'] = '1'
- cliq_args['verbose'] = '0'
-
- result_xml = self._cliq_run_xml("getClusterInfo", cliq_args)
-
- return result_xml
-
- def _cliq_get_cluster_vip(self, cluster_name):
- """Gets the IP on which a cluster shares iSCSI volumes."""
- cluster_xml = self._cliq_get_cluster_info(cluster_name)
-
- vips = []
- for vip in cluster_xml.findall("response/cluster/vip"):
- vips.append(vip.attrib.get('ipAddress'))
-
- if len(vips) == 1:
- return vips[0]
-
- _xml = etree.tostring(cluster_xml)
- msg = (_("Unexpected number of virtual ips for cluster "
- " %(cluster_name)s. Result=%(_xml)s") %
- {'cluster_name': cluster_name, '_xml': _xml})
- raise exception.VolumeBackendAPIException(data=msg)
-
- def _cliq_get_volume_info(self, volume_name):
- """Gets the volume info, including IQN."""
- cliq_args = {}
- cliq_args['volumeName'] = volume_name
- result_xml = self._cliq_run_xml("getVolumeInfo", cliq_args)
-
- # Result looks like this:
- # <gauche version="1.0">
- # <response description="Operation succeeded." name="CliqSuccess"
- # processingTime="87" result="0">
- # <volume autogrowPages="4" availability="online" blockSize="1024"
- # bytesWritten="0" checkSum="false" clusterName="Cluster01"
- # created="2011-02-08T19:56:53Z" deleting="false" description=""
- # groupName="Group01" initialQuota="536870912" isPrimary="true"
- # iscsiIqn="iqn.2003-10.com.lefthandnetworks:group01:25366:vol-b"
- # maxSize="6865387257856" md5="9fa5c8b2cca54b2948a63d833097e1ca"
- # minReplication="1" name="vol-b" parity="0" replication="2"
- # reserveQuota="536870912" scratchQuota="4194304"
- # serialNumber="9fa5c8b2cca54b2948a63d833097e1ca0000000000006316"
- # size="1073741824" stridePages="32" thinProvision="true">
- # <status description="OK" value="2"/>
- # <permission access="rw"
- # authGroup="api-34281B815713B78-(trimmed)51ADD4B7030853AA7"
- # chapName="chapusername" chapRequired="true" id="25369"
- # initiatorSecret="" iqn="" iscsiEnabled="true"
- # loadBalance="true" targetSecret="supersecret"/>
- # </volume>
- # </response>
- # </gauche>
-
- # Flatten the nodes into a dictionary; use prefixes to avoid collisions
- volume_attributes = {}
-
- volume_node = result_xml.find("response/volume")
- for k, v in volume_node.attrib.items():
- volume_attributes["volume." + k] = v
-
- status_node = volume_node.find("status")
- if status_node is not None:
- for k, v in status_node.attrib.items():
- volume_attributes["status." + k] = v
-
- # We only consider the first permission node
- permission_node = volume_node.find("permission")
- if permission_node is not None:
- for k, v in status_node.attrib.items():
- volume_attributes["permission." + k] = v
-
- LOG.debug("Volume info: %(volume_name)s => %(volume_attributes)s",
- {'volume_name': volume_name,
- 'volume_attributes': volume_attributes})
- return volume_attributes
-
- def _cliq_get_snapshot_info(self, snapshot_name):
- """Gets the snapshot info, including IQN."""
- cliq_args = {}
- cliq_args['snapshotName'] = snapshot_name
- result_xml = self._cliq_run_xml("getSnapshotInfo", cliq_args)
-
- # Result looks like this:
- # <gauche version="1.0">
- # <response description="Operation succeeded." name="CliqSuccess"
- # processingTime="87" result="0">
- # <snapshot applicationManaged="false" autogrowPages="32768"
- # automatic="false" availability="online" bytesWritten="0"
- # clusterName="CloudCluster1" created="2013-08-26T07:03:44Z"
- # deleting="false" description="" groupName="CloudMgmtGroup1"
- # id="730" initialQuota="536870912" isPrimary="true"
- # iscsiIqn="iqn.2003-10.com.lefthandnetworks:cloudmgmtgroup1:73"
- # md5="a64b4f850539c07fb5ce3cee5db1fcce" minReplication="1"
- # name="snapshot-7849288e-e5e8-42cb-9687-9af5355d674b"
- # replication="2" reserveQuota="536870912" scheduleId="0"
- # scratchQuota="4194304" scratchWritten="0"
- # serialNumber="a64b4f850539c07fb5ce3cee5db1fcce00000000000002da"
- # size="2147483648" stridePages="32"
- # volumeSerial="a64b4f850539c07fb5ce3cee5db1fcce00000000000002d">
- # <status description="OK" value="2"/>
- # <permission access="rw"
- # authGroup="api-34281B815713B78-(trimmed)51ADD4B7030853AA7"
- # chapName="chapusername" chapRequired="true" id="25369"
- # initiatorSecret="" iqn="" iscsiEnabled="true"
- # loadBalance="true" targetSecret="supersecret"/>
- # </snapshot>
- # </response>
- # </gauche>
-
- # Flatten the nodes into a dictionary; use prefixes to avoid collisions
- snapshot_attributes = {}
-
- snapshot_node = result_xml.find("response/snapshot")
- for k, v in snapshot_node.attrib.items():
- snapshot_attributes["snapshot." + k] = v
-
- status_node = snapshot_node.find("status")
- if status_node is not None:
- for k, v in status_node.attrib.items():
- snapshot_attributes["status." + k] = v
-
- # We only consider the first permission node
- permission_node = snapshot_node.find("permission")
- if permission_node is not None:
- for k, v in status_node.attrib.items():
- snapshot_attributes["permission." + k] = v
-
- LOG.debug("Snapshot info: %(name)s => %(attributes)s",
- {'name': snapshot_name, 'attributes': snapshot_attributes})
- return snapshot_attributes
-
- def create_volume(self, volume):
- """Creates a volume."""
- cliq_args = {}
- cliq_args['clusterName'] = self.configuration.san_clustername
-
- if self.configuration.san_thin_provision:
- cliq_args['thinProvision'] = '1'
- else:
- cliq_args['thinProvision'] = '0'
-
- cliq_args['volumeName'] = volume['name']
- cliq_args['size'] = '%sGB' % volume['size']
-
- self._cliq_run_xml("createVolume", cliq_args)
-
- return self._get_model_update(volume['name'])
-
- def extend_volume(self, volume, new_size):
- """Extend the size of an existing volume."""
- cliq_args = {}
- cliq_args['volumeName'] = volume['name']
- cliq_args['size'] = '%sGB' % new_size
-
- self._cliq_run_xml("modifyVolume", cliq_args)
-
- def create_volume_from_snapshot(self, volume, snapshot):
- """Creates a volume from a snapshot."""
- cliq_args = {}
- cliq_args['snapshotName'] = snapshot['name']
- cliq_args['volumeName'] = volume['name']
-
- self._cliq_run_xml("cloneSnapshot", cliq_args)
-
- return self._get_model_update(volume['name'])
-
- def create_snapshot(self, snapshot):
- """Creates a snapshot."""
- cliq_args = {}
- cliq_args['snapshotName'] = snapshot['name']
- cliq_args['volumeName'] = snapshot['volume_name']
- cliq_args['inheritAccess'] = 1
- self._cliq_run_xml("createSnapshot", cliq_args)
-
- def delete_volume(self, volume):
- """Deletes a volume."""
- cliq_args = {}
- cliq_args['volumeName'] = volume['name']
- cliq_args['prompt'] = 'false' # Don't confirm
- try:
- self._cliq_get_volume_info(volume['name'])
- except processutils.ProcessExecutionError:
- LOG.error(_LE("Volume did not exist. It will not be deleted"))
- return
- self._cliq_run_xml("deleteVolume", cliq_args)
-
- def delete_snapshot(self, snapshot):
- """Deletes a snapshot."""
- cliq_args = {}
- cliq_args['snapshotName'] = snapshot['name']
- cliq_args['prompt'] = 'false' # Don't confirm
- try:
- self._cliq_get_snapshot_info(snapshot['name'])
- except processutils.ProcessExecutionError:
- LOG.error(_LE("Snapshot did not exist. It will not be deleted"))
- return
- try:
- self._cliq_run_xml("deleteSnapshot", cliq_args)
- except Exception as ex:
- in_use_msg = 'cannot be deleted because it is a clone point'
- if in_use_msg in ex.message:
- raise exception.SnapshotIsBusy(ex)
-
- raise exception.VolumeBackendAPIException(ex)
-
- def local_path(self, volume):
- msg = _("local_path not supported")
- raise exception.VolumeBackendAPIException(data=msg)
-
- def initialize_connection(self, volume, connector):
- """Assigns the volume to a server.
-
- Assign any created volume to a compute node/host so that it can be
- used from that host. HP VSA requires a volume to be assigned
- to a server.
-
- This driver returns a driver_volume_type of 'iscsi'.
- The format of the driver data is defined in _get_iscsi_properties.
- Example return value:
-
- {
- 'driver_volume_type': 'iscsi'
- 'data': {
- 'target_discovered': True,
- 'target_iqn': 'iqn.2010-10.org.openstack:volume-00000001',
- 'target_protal': '127.0.0.1:3260',
- 'volume_id': 1,
- }
- }
-
- """
- self._create_server(connector)
- cliq_args = {}
- cliq_args['volumeName'] = volume['name']
- cliq_args['serverName'] = connector['host']
- self._cliq_run_xml("assignVolumeToServer", cliq_args)
-
- iscsi_data = self._get_iscsi_properties(volume)
- return {
- 'driver_volume_type': 'iscsi',
- 'data': iscsi_data
- }
-
- def _create_server(self, connector):
- cliq_args = {}
- cliq_args['serverName'] = connector['host']
- out = self._cliq_run_xml("getServerInfo", cliq_args, False)
- response = out.find("response")
- result = response.attrib.get("result")
- if result != '0':
- cliq_args = {}
- cliq_args['serverName'] = connector['host']
- cliq_args['initiator'] = connector['initiator']
- self._cliq_run_xml("createServer", cliq_args)
-
- def _get_model_update(self, volume_name):
- volume_info = self._cliq_get_volume_info(volume_name)
- cluster_name = volume_info['volume.clusterName']
- iscsi_iqn = volume_info['volume.iscsiIqn']
-
- # TODO(justinsb): Is this always 1? Does it matter?
- cluster_interface = '1'
-
- if not self.cluster_vip:
- self.cluster_vip = self._cliq_get_cluster_vip(cluster_name)
- iscsi_portal = self.cluster_vip + ":3260," + cluster_interface
-
- model_update = {}
-
- # NOTE(jdg): LH volumes always at lun 0 ?
- model_update['provider_location'] = ("%s %s %s" %
- (iscsi_portal,
- iscsi_iqn,
- 0))
- return model_update
-
- def terminate_connection(self, volume, connector, **kwargs):
- """Unassign the volume from the host."""
- cliq_args = {}
- cliq_args['volumeName'] = volume['name']
- cliq_args['serverName'] = connector['host']
- self._cliq_run_xml("unassignVolumeToServer", cliq_args)
-
- def get_volume_stats(self, refresh=False):
- if refresh:
- self._update_backend_status()
-
- return self.device_stats
-
- def _update_backend_status(self):
- data = {}
- backend_name = self.configuration.safe_get('volume_backend_name')
- data['volume_backend_name'] = backend_name or self.__class__.__name__
- data['reserved_percentage'] = 0
- data['storage_protocol'] = 'iSCSI'
- data['vendor_name'] = 'Hewlett-Packard'
-
- result_xml = self._cliq_run_xml(
- "getClusterInfo", {
- 'searchDepth': 1,
- 'clusterName': self.configuration.san_clustername})
- cluster_node = result_xml.find("response/cluster")
- total_capacity = cluster_node.attrib.get("spaceTotal")
- free_capacity = cluster_node.attrib.get("unprovisionedSpace")
- GB = units.Gi
-
- data['total_capacity_gb'] = int(total_capacity) / GB
- data['free_capacity_gb'] = int(free_capacity) / GB
- self.device_stats = data
-
- def create_cloned_volume(self, volume, src_vref):
- raise NotImplementedError()
-
- def create_export(self, context, volume, connector):
- pass
-
- def ensure_export(self, context, volume):
- pass
-
- def remove_export(self, context, volume):
- pass
-
- def retype(self, context, volume, new_type, diff, host):
- """Convert the volume to be of the new type.
-
- Returns a boolean indicating whether the retype occurred.
-
- :param ctxt: Context
- :param volume: A dictionary describing the volume to migrate
- :param new_type: A dictionary describing the volume type to convert to
- :param diff: A dictionary with the difference between the two types
- """
- return False
-
- def migrate_volume(self, ctxt, volume, host):
- """Migrate the volume to the specified host.
-
- Returns a boolean indicating whether the migration occurred, as well as
- model_update.
-
- :param ctxt: Context
- :param volume: A dictionary describing the volume to migrate
- :param host: A dictionary describing the host to migrate to, where
- host['host'] is its name, and host['capabilities'] is a
- dictionary of its reported capabilities.
- """
- return (False, None)
-
- def update_migrated_volume(self, context, volume, new_volume,
- original_volume_status):
- raise NotImplementedError()
-# (c) Copyright 2014 Hewlett-Packard Development Company, L.P.
+# (c) Copyright 2014-2015 Hewlett-Packard Development Company, L.P.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# License for the specific language governing permissions and limitations
# under the License.
#
-"""
-Volume driver for HP LeftHand Storage array.
-This driver requires 11.5 or greater firmware on the LeftHand array, using
-the 1.0 or greater version of the hplefthandclient.
-
-You will need to install the python hplefthandclient.
-sudo pip install hplefthandclient
-
-Set the following in the cinder.conf file to enable the
-LeftHand Channel Driver along with the required flags:
-
-volume_driver=cinder.volume.drivers.san.hp.hp_lefthand_iscsi.
- HPLeftHandISCSIDriver
-
-It also requires the setting of hplefthand_api_url, hplefthand_username,
-hplefthand_password for credentials to talk to the REST service on the
-LeftHand array.
-"""
+"""HP LeftHand SAN ISCSI REST Proxy."""
+from oslo_config import cfg
from oslo_log import log as logging
+from oslo_utils import excutils
+from oslo_utils import importutils
+from oslo_utils import units
+from cinder import context
from cinder import exception
-from cinder.i18n import _, _LI
+from cinder.i18n import _, _LE, _LI, _LW
+from cinder import objects
from cinder.volume import driver
-from cinder.volume.drivers.san.hp import hp_lefthand_cliq_proxy as cliq_proxy
-from cinder.volume.drivers.san.hp import hp_lefthand_rest_proxy as rest_proxy
+from cinder.volume import utils
+from cinder.volume import volume_types
+
+import six
+
+import math
+import re
LOG = logging.getLogger(__name__)
+hplefthandclient = importutils.try_import("hplefthandclient")
+if hplefthandclient:
+ from hplefthandclient import client as hp_lh_client
+ from hplefthandclient import exceptions as hpexceptions
+
+hplefthand_opts = [
+ cfg.StrOpt('hplefthand_api_url',
+ help="HP LeftHand WSAPI Server Url like "
+ "https://<LeftHand ip>:8081/lhos"),
+ cfg.StrOpt('hplefthand_username',
+ help="HP LeftHand Super user username"),
+ cfg.StrOpt('hplefthand_password',
+ help="HP LeftHand Super user password",
+ secret=True),
+ cfg.StrOpt('hplefthand_clustername',
+ help="HP LeftHand cluster name"),
+ cfg.BoolOpt('hplefthand_iscsi_chap_enabled',
+ default=False,
+ help='Configure CHAP authentication for iSCSI connections '
+ '(Default: Disabled)'),
+ cfg.BoolOpt('hplefthand_debug',
+ default=False,
+ help="Enable HTTP debugging to LeftHand"),
+
+]
+
+CONF = cfg.CONF
+CONF.register_opts(hplefthand_opts)
+
+MIN_API_VERSION = "1.1"
MIN_CLIENT_VERSION = '1.0.4'
+MIN_CG_CLIENT_VERSION = "1.0.6"
+# map the extra spec key to the REST client option key
+extra_specs_key_map = {
+ 'hplh:provisioning': 'isThinProvisioned',
+ 'hplh:ao': 'isAdaptiveOptimizationEnabled',
+ 'hplh:data_pl': 'dataProtectionLevel',
+}
-class HPLeftHandISCSIDriver(driver.TransferVD,
- driver.ManageableVD,
- driver.ExtendVD,
- driver.SnapshotVD,
- driver.MigrateVD,
- driver.BaseVD,
- driver.ConsistencyGroupVD):
- """Executes commands relating to HP/LeftHand SAN ISCSI volumes.
+# map the extra spec value to the REST client option value
+extra_specs_value_map = {
+ 'isThinProvisioned': {'thin': True, 'full': False},
+ 'isAdaptiveOptimizationEnabled': {'true': True, 'false': False},
+ 'dataProtectionLevel': {
+ 'r-0': 0, 'r-5': 1, 'r-10-2': 2, 'r-10-3': 3, 'r-10-4': 4, 'r-6': 5}
+}
+
+
+class HPLeftHandISCSIDriver(driver.ISCSIDriver):
+ """Executes REST commands relating to HP/LeftHand SAN ISCSI volumes.
Version history:
- 1.0.0 - Initial driver
+ 1.0.0 - Initial REST iSCSI proxy
1.0.1 - Added support for retype
1.0.2 - Added support for volume migrate
- 1.0.3 - Fix for no handler for logger during tests
- 1.0.4 - Removing locks bug #1395953
- 1.0.5 - Adding support for manage/unmanage.
- 1.0.6 - Updated minimum client version. bug #1432757
- 1.0.7 - Update driver to use ABC metaclasses
- 1.0.8 - Adds consistency group support
- 1.0.9 - Added update_migrated_volume #1493546
+ 1.0.3 - Fixed bug #1285829, HP LeftHand backend assisted migration
+ should check for snapshots
+ 1.0.4 - Fixed bug #1285925, LeftHand AO volume create performance
+ improvement
+ 1.0.5 - Fixed bug #1311350, Live-migration of an instance when
+ attached to a volume was causing an error.
+ 1.0.6 - Removing locks bug #1395953
+ 1.0.7 - Fixed bug #1353137, Server was not removed from the HP
+ Lefthand backend after the last volume was detached.
+ 1.0.8 - Fixed bug #1418201, A cloned volume fails to attach.
+ 1.0.9 - Adding support for manage/unmanage.
+ 1.0.10 - Add stats for goodness_function and filter_function
+ 1.0.11 - Add over subscription support
+ 1.0.12 - Adds consistency group support
+ 1.0.13 - Added update_migrated_volume #1493546
+ 1.0.14 - Removed the old CLIQ based driver
"""
- VERSION = "1.0.9"
+ VERSION = "1.0.14"
+
+ device_stats = {}
def __init__(self, *args, **kwargs):
super(HPLeftHandISCSIDriver, self).__init__(*args, **kwargs)
- self.proxy = None
- self.args = args
- self.kwargs = kwargs
+ self.configuration.append_config_values(hplefthand_opts)
+ if not self.configuration.hplefthand_api_url:
+ raise exception.NotFound(_("HPLeftHand url not found"))
- def _create_proxy(self, *args, **kwargs):
+ # blank is the only invalid character for cluster names
+ # so we need to use it as a separator
+ self.DRIVER_LOCATION = self.__class__.__name__ + ' %(cluster)s %(vip)s'
+ self.db = kwargs.get('db')
+
+ def _login(self):
+ client = self._create_client()
try:
- proxy = rest_proxy.HPLeftHandRESTProxy(*args, **kwargs)
- except exception.NotFound:
- proxy = cliq_proxy.HPLeftHandCLIQProxy(*args, **kwargs)
+ if self.configuration.hplefthand_debug:
+ client.debug_rest(True)
- return proxy
+ client.login(
+ self.configuration.hplefthand_username,
+ self.configuration.hplefthand_password)
- def check_for_setup_error(self):
- self.proxy.check_for_setup_error()
+ cluster_info = client.getClusterByName(
+ self.configuration.hplefthand_clustername)
+ self.cluster_id = cluster_info['id']
+ virtual_ips = cluster_info['virtualIPAddresses']
+ self.cluster_vip = virtual_ips[0]['ipV4Address']
+
+ return client
+ except hpexceptions.HTTPNotFound:
+ raise exception.DriverNotInitialized(
+ _('LeftHand cluster not found'))
+ except Exception as ex:
+ raise exception.DriverNotInitialized(ex)
+
+ def _logout(self, client):
+ client.logout()
+
+ def _create_client(self):
+ return hp_lh_client.HPLeftHandClient(
+ self.configuration.hplefthand_api_url)
def do_setup(self, context):
- self.proxy = self._create_proxy(*self.args, **self.kwargs)
+ """Set up LeftHand client."""
+ if hplefthandclient.version < MIN_CLIENT_VERSION:
+ ex_msg = (_("Invalid hplefthandclient version found ("
+ "%(found)s). Version %(minimum)s or greater "
+ "required.")
+ % {'found': hplefthandclient.version,
+ 'minimum': MIN_CLIENT_VERSION})
+ LOG.error(ex_msg)
+ raise exception.InvalidInput(reason=ex_msg)
+
+ def check_for_setup_error(self):
+ """Checks for incorrect LeftHand API being used on backend."""
+ client = self._login()
+ try:
+ self.api_version = client.getApiVersion()
- LOG.info(_LI("HPLeftHand driver %(driver_ver)s, "
- "proxy %(proxy_ver)s"), {
- "driver_ver": self.VERSION,
- "proxy_ver": self.proxy.get_version_string()})
+ LOG.info(_LI("HPLeftHand API version %s"), self.api_version)
- if isinstance(self.proxy, cliq_proxy.HPLeftHandCLIQProxy):
- self.proxy.do_setup(context)
- else:
- # Check minimum client version for REST proxy
- client_version = rest_proxy.hplefthandclient.version
-
- if client_version < MIN_CLIENT_VERSION:
- ex_msg = (_("Invalid hplefthandclient version found ("
- "%(found)s). Version %(minimum)s or greater "
- "required.")
- % {'found': client_version,
- 'minimum': MIN_CLIENT_VERSION})
- LOG.error(ex_msg)
- raise exception.InvalidInput(reason=ex_msg)
+ if self.api_version < MIN_API_VERSION:
+ LOG.warning(_LW("HPLeftHand API is version %(current)s. "
+ "A minimum version of %(min)s is needed for "
+ "manage/unmanage support."),
+ {'current': self.api_version,
+ 'min': MIN_API_VERSION})
+ finally:
+ self._logout(client)
+
+ def get_version_string(self):
+ return (_('REST %(proxy_ver)s hplefthandclient %(rest_ver)s') % {
+ 'proxy_ver': self.VERSION,
+ 'rest_ver': hplefthandclient.get_version_string()})
def create_volume(self, volume):
"""Creates a volume."""
- return self.proxy.create_volume(volume)
+ client = self._login()
+ try:
+ # get the extra specs of interest from this volume's volume type
+ volume_extra_specs = self._get_volume_extra_specs(volume)
+ extra_specs = self._get_lh_extra_specs(
+ volume_extra_specs,
+ extra_specs_key_map.keys())
+
+ # map the extra specs key/value pairs to key/value pairs
+ # used as optional configuration values by the LeftHand backend
+ optional = self._map_extra_specs(extra_specs)
+
+ # if provisioning is not set, default to thin
+ if 'isThinProvisioned' not in optional:
+ optional['isThinProvisioned'] = True
+
+ # AdaptiveOptimization defaults to 'true' if you don't specify the
+ # value on a create, and that is the most efficient way to create
+ # a volume. If you pass in 'false' or 'true' for AO, it will result
+ # in an update operation following the create operation to set this
+ # value, so it is best to not specify the value and let it default
+ # to 'true'.
+ if optional.get('isAdaptiveOptimizationEnabled'):
+ del optional['isAdaptiveOptimizationEnabled']
+
+ clusterName = self.configuration.hplefthand_clustername
+ optional['clusterName'] = clusterName
+
+ volume_info = client.createVolume(
+ volume['name'], self.cluster_id,
+ volume['size'] * units.Gi,
+ optional)
+
+ return self._update_provider(volume_info)
+ except Exception as ex:
+ raise exception.VolumeBackendAPIException(data=ex)
+ finally:
+ self._logout(client)
+
+ def delete_volume(self, volume):
+ """Deletes a volume."""
+ client = self._login()
+ try:
+ volume_info = client.getVolumeByName(volume['name'])
+ client.deleteVolume(volume_info['id'])
+ except hpexceptions.HTTPNotFound:
+ LOG.error(_LE("Volume did not exist. It will not be deleted"))
+ except Exception as ex:
+ raise exception.VolumeBackendAPIException(ex)
+ finally:
+ self._logout(client)
def extend_volume(self, volume, new_size):
"""Extend the size of an existing volume."""
- self.proxy.extend_volume(volume, new_size)
+ client = self._login()
+ try:
+ volume_info = client.getVolumeByName(volume['name'])
+
+ # convert GB to bytes
+ options = {'size': int(new_size) * units.Gi}
+ client.modifyVolume(volume_info['id'], options)
+ except Exception as ex:
+ raise exception.VolumeBackendAPIException(ex)
+ finally:
+ self._logout(client)
def create_consistencygroup(self, context, group):
- """Creates a consistency group."""
- return self.proxy.create_consistencygroup(context, group)
+ """Creates a consistencygroup."""
+ model_update = {'status': 'available'}
+ return model_update
def create_consistencygroup_from_src(self, context, group, volumes,
cgsnapshot=None, snapshots=None,
source_cg=None, source_vols=None):
"""Creates a consistency group from a source"""
- return self.proxy.create_consistencygroup_from_src(
- context, group, volumes, cgsnapshot, snapshots, source_cg,
- source_vols)
+ msg = _("Creating a consistency group from a source is not "
+ "currently supported.")
+ LOG.error(msg)
+ raise NotImplementedError(msg)
def delete_consistencygroup(self, context, group, volumes):
"""Deletes a consistency group."""
- return self.proxy.delete_consistencygroup(context, group)
+ # TODO(aorourke): Can't eliminate the DB calls here due to CG API.
+ # Will fix in M release
+ volumes = self.db.volume_get_all_by_group(context, group.id)
+ for volume in volumes:
+ self.delete_volume(volume)
+ volume.status = 'deleted'
+
+ model_update = {'status': group.status}
+
+ return model_update, volumes
def update_consistencygroup(self, context, group,
add_volumes=None, remove_volumes=None):
- """Updates a consistency group."""
- return self.proxy.update_consistencygroup(context, group, add_volumes,
- remove_volumes)
+ """Updates a consistency group.
+
+ Because the backend has no concept of volume grouping, cinder will
+ maintain all volume/consistency group relationships. Because of this
+ functionality, there is no need to make any client calls; instead
+ simply returning out of this function allows cinder to properly
+ add/remove volumes from the consistency group.
+ """
+ return None, None, None
def create_cgsnapshot(self, context, cgsnapshot, snapshots):
"""Creates a consistency group snapshot."""
- return self.proxy.create_cgsnapshot(context, cgsnapshot)
+ client = self._login()
+ try:
+ # TODO(aorourke): Can't eliminate the DB calls here due to CG API.
+ # Will fix in M release
+ snapshots = objects.SnapshotList().get_all_for_cgsnapshot(
+ context, cgsnapshot['id'])
+
+ snap_set = []
+ snapshot_base_name = "snapshot-" + cgsnapshot['id']
+ for i, snapshot in enumerate(snapshots):
+ volume = snapshot.volume
+ volume_name = volume['name']
+ try:
+ volume_info = client.getVolumeByName(volume_name)
+ except Exception as ex:
+ error = six.text_type(ex)
+ LOG.error(_LE("Could not find volume with name %(name)s. "
+ "Error: %(error)s"),
+ {'name': volume_name,
+ 'error': error})
+ raise exception.VolumeBackendAPIException(data=error)
+
+ volume_id = volume_info['id']
+ snapshot_name = snapshot_base_name + "-" + six.text_type(i)
+ snap_set_member = {'volumeName': volume_name,
+ 'volumeId': volume_id,
+ 'snapshotName': snapshot_name}
+ snap_set.append(snap_set_member)
+ snapshot.status = 'available'
+
+ source_volume_id = snap_set[0]['volumeId']
+ optional = {'inheritAccess': True}
+ description = cgsnapshot.get('description', None)
+ if description:
+ optional['description'] = description
+
+ try:
+ client.createSnapshotSet(source_volume_id, snap_set, optional)
+ except Exception as ex:
+ error = six.text_type(ex)
+ LOG.error(_LE("Could not create snapshot set. Error: '%s'"),
+ error)
+ raise exception.VolumeBackendAPIException(
+ data=error)
+
+ except Exception as ex:
+ raise exception.VolumeBackendAPIException(data=six.text_type(ex))
+ finally:
+ self._logout(client)
+
+ model_update = {'status': 'available'}
+
+ return model_update, snapshots
def delete_cgsnapshot(self, context, cgsnapshot, snapshots):
"""Deletes a consistency group snapshot."""
- return self.proxy.delete_cgsnapshot(context, cgsnapshot)
- def create_volume_from_snapshot(self, volume, snapshot):
- """Creates a volume from a snapshot."""
- return self.proxy.create_volume_from_snapshot(volume, snapshot)
+ client = self._login()
+ try:
+ snap_name_base = "snapshot-" + cgsnapshot['id']
+
+ # TODO(aorourke): Can't eliminate the DB calls here due to CG API.
+ # Will fix in M release
+ snapshots = objects.SnapshotList().get_all_for_cgsnapshot(
+ context, cgsnapshot['id'])
+
+ for i, snapshot in enumerate(snapshots):
+ try:
+ snap_name = snap_name_base + "-" + six.text_type(i)
+ snap_info = client.getSnapshotByName(snap_name)
+ client.deleteSnapshot(snap_info['id'])
+ except hpexceptions.HTTPNotFound:
+ LOG.error(_LE("Snapshot did not exist. It will not be "
+ "deleted."))
+ except hpexceptions.HTTPServerError as ex:
+ in_use_msg = ('cannot be deleted because it is a clone '
+ 'point')
+ if in_use_msg in ex.get_description():
+ raise exception.SnapshotIsBusy(snapshot_name=snap_name)
+
+ raise exception.VolumeBackendAPIException(
+ data=six.text_type(ex))
+
+ except Exception as ex:
+ raise exception.VolumeBackendAPIException(
+ data=six.text_type(ex))
+ finally:
+ self._logout(client)
+
+ model_update = {'status': cgsnapshot['status']}
+
+ return model_update, snapshots
def create_snapshot(self, snapshot):
"""Creates a snapshot."""
- self.proxy.create_snapshot(snapshot)
+ client = self._login()
+ try:
+ volume_info = client.getVolumeByName(snapshot['volume_name'])
- def delete_volume(self, volume):
- """Deletes a volume."""
- self.proxy.delete_volume(volume)
+ option = {'inheritAccess': True}
+ client.createSnapshot(snapshot['name'],
+ volume_info['id'],
+ option)
+ except Exception as ex:
+ raise exception.VolumeBackendAPIException(ex)
+ finally:
+ self._logout(client)
def delete_snapshot(self, snapshot):
"""Deletes a snapshot."""
- self.proxy.delete_snapshot(snapshot)
+ client = self._login()
+ try:
+ snap_info = client.getSnapshotByName(snapshot['name'])
+ client.deleteSnapshot(snap_info['id'])
+ except hpexceptions.HTTPNotFound:
+ LOG.error(_LE("Snapshot did not exist. It will not be deleted"))
+ except hpexceptions.HTTPServerError as ex:
+ in_use_msg = 'cannot be deleted because it is a clone point'
+ if in_use_msg in ex.get_description():
+ raise exception.SnapshotIsBusy(snapshot_name=snapshot['name'])
+
+ raise exception.VolumeBackendAPIException(ex)
+
+ except Exception as ex:
+ raise exception.VolumeBackendAPIException(ex)
+ finally:
+ self._logout(client)
+
+ def get_volume_stats(self, refresh=False):
+ """Gets volume stats."""
+ client = self._login()
+ try:
+ if refresh:
+ self._update_backend_status(client)
+
+ return self.device_stats
+ finally:
+ self._logout(client)
+
+ def _update_backend_status(self, client):
+ data = {}
+ backend_name = self.configuration.safe_get('volume_backend_name')
+ data['driver_version'] = self.VERSION
+ data['volume_backend_name'] = backend_name or self.__class__.__name__
+ data['reserved_percentage'] = (
+ self.configuration.safe_get('reserved_percentage'))
+ data['storage_protocol'] = 'iSCSI'
+ data['vendor_name'] = 'Hewlett-Packard'
+ data['location_info'] = (self.DRIVER_LOCATION % {
+ 'cluster': self.configuration.hplefthand_clustername,
+ 'vip': self.cluster_vip})
+ data['thin_provisioning_support'] = True
+ data['thick_provisioning_support'] = True
+ data['max_over_subscription_ratio'] = (
+ self.configuration.safe_get('max_over_subscription_ratio'))
+
+ cluster_info = client.getCluster(self.cluster_id)
+
+ total_capacity = cluster_info['spaceTotal']
+ free_capacity = cluster_info['spaceAvailable']
+
+ # convert to GB
+ data['total_capacity_gb'] = int(total_capacity) / units.Gi
+ data['free_capacity_gb'] = int(free_capacity) / units.Gi
+
+ # Collect some stats
+ capacity_utilization = (
+ (float(total_capacity - free_capacity) /
+ float(total_capacity)) * 100)
+ # Don't have a better way to get the total number volumes
+ # so try to limit the size of data for now. Once new lefthand API is
+ # available, replace this call.
+ total_volumes = 0
+ provisioned_size = 0
+ volumes = client.getVolumes(
+ cluster=self.configuration.hplefthand_clustername,
+ fields=['members[id]', 'members[clusterName]', 'members[size]'])
+ if volumes:
+ total_volumes = volumes['total']
+ provisioned_size = sum(
+ members['size'] for members in volumes['members'])
+ data['provisioned_capacity_gb'] = int(provisioned_size) / units.Gi
+ data['capacity_utilization'] = capacity_utilization
+ data['total_volumes'] = total_volumes
+ data['filter_function'] = self.get_filter_function()
+ data['goodness_function'] = self.get_goodness_function()
+ if hplefthandclient.version >= MIN_CG_CLIENT_VERSION:
+ data['consistencygroup_support'] = True
+
+ self.device_stats = data
def initialize_connection(self, volume, connector):
- """Assigns the volume to a server."""
- return self.proxy.initialize_connection(volume, connector)
+ """Assigns the volume to a server.
+
+ Assign any created volume to a compute node/host so that it can be
+ used from that host. HP VSA requires a volume to be assigned
+ to a server.
+ """
+ client = self._login()
+ try:
+ server_info = self._create_server(connector, client)
+ volume_info = client.getVolumeByName(volume['name'])
+
+ access_already_enabled = False
+ if volume_info['iscsiSessions'] is not None:
+ # Extract the server id for each session to check if the
+ # new server already has access permissions enabled.
+ for session in volume_info['iscsiSessions']:
+ server_id = int(session['server']['uri'].split('/')[3])
+ if server_id == server_info['id']:
+ access_already_enabled = True
+ break
+
+ if not access_already_enabled:
+ client.addServerAccess(
+ volume_info['id'],
+ server_info['id'])
+
+ iscsi_properties = self._get_iscsi_properties(volume)
+
+ if ('chapAuthenticationRequired' in server_info and
+ server_info['chapAuthenticationRequired']):
+ iscsi_properties['auth_method'] = 'CHAP'
+ iscsi_properties['auth_username'] = connector['initiator']
+ iscsi_properties['auth_password'] = (
+ server_info['chapTargetSecret'])
+
+ return {'driver_volume_type': 'iscsi', 'data': iscsi_properties}
+ except Exception as ex:
+ raise exception.VolumeBackendAPIException(ex)
+ finally:
+ self._logout(client)
def terminate_connection(self, volume, connector, **kwargs):
"""Unassign the volume from the host."""
- self.proxy.terminate_connection(volume, connector)
+ client = self._login()
+ try:
+ volume_info = client.getVolumeByName(volume['name'])
+ server_info = client.getServerByName(connector['host'])
+ volume_list = client.findServerVolumes(server_info['name'])
- def get_volume_stats(self, refresh=False):
- data = self.proxy.get_volume_stats(refresh)
- data['driver_version'] = self.VERSION
- return data
+ removeServer = True
+ for entry in volume_list:
+ if entry['id'] != volume_info['id']:
+ removeServer = False
+ break
+
+ client.removeServerAccess(
+ volume_info['id'],
+ server_info['id'])
+
+ if removeServer:
+ client.deleteServer(server_info['id'])
+ except Exception as ex:
+ raise exception.VolumeBackendAPIException(ex)
+ finally:
+ self._logout(client)
+
+ def create_volume_from_snapshot(self, volume, snapshot):
+ """Creates a volume from a snapshot."""
+ client = self._login()
+ try:
+ snap_info = client.getSnapshotByName(snapshot['name'])
+ volume_info = client.cloneSnapshot(
+ volume['name'],
+ snap_info['id'])
+ return self._update_provider(volume_info)
+ except Exception as ex:
+ raise exception.VolumeBackendAPIException(ex)
+ finally:
+ self._logout(client)
def create_cloned_volume(self, volume, src_vref):
- return self.proxy.create_cloned_volume(volume, src_vref)
+ client = self._login()
+ try:
+ volume_info = client.getVolumeByName(src_vref['name'])
+ clone_info = client.cloneVolume(volume['name'], volume_info['id'])
+ return self._update_provider(clone_info)
+ except Exception as ex:
+ raise exception.VolumeBackendAPIException(ex)
+ finally:
+ self._logout(client)
+
+ def _get_volume_extra_specs(self, volume):
+ """Get extra specs from a volume."""
+ extra_specs = {}
+ type_id = volume.get('volume_type_id', None)
+ if type_id is not None:
+ ctxt = context.get_admin_context()
+ volume_type = volume_types.get_volume_type(ctxt, type_id)
+ extra_specs = volume_type.get('extra_specs')
+ return extra_specs
+
+ def _get_lh_extra_specs(self, extra_specs, valid_keys):
+ """Get LeftHand extra_specs (valid_keys only)."""
+ extra_specs_of_interest = {}
+ for key, value in extra_specs.items():
+ if key in valid_keys:
+ extra_specs_of_interest[key] = value
+ return extra_specs_of_interest
+
+ def _map_extra_specs(self, extra_specs):
+ """Map the extra spec key/values to LeftHand key/values."""
+ client_options = {}
+ for key, value in extra_specs.items():
+ # map extra spec key to lh client option key
+ client_key = extra_specs_key_map[key]
+ # map extra spect value to lh client option value
+ try:
+ value_map = extra_specs_value_map[client_key]
+ # an invalid value will throw KeyError
+ client_value = value_map[value]
+ client_options[client_key] = client_value
+ except KeyError:
+ LOG.error(_LE("'%(value)s' is an invalid value "
+ "for extra spec '%(key)s'"),
+ {'value': value, 'key': key})
+ return client_options
+
+ def _update_provider(self, volume_info):
+ # TODO(justinsb): Is this always 1? Does it matter?
+ cluster_interface = '1'
+ iscsi_portal = self.cluster_vip + ":3260," + cluster_interface
+
+ return {'provider_location': (
+ "%s %s %s" % (iscsi_portal, volume_info['iscsiIqn'], 0))}
+
+ def _create_server(self, connector, client):
+ server_info = None
+ chap_enabled = self.configuration.hplefthand_iscsi_chap_enabled
+ try:
+ server_info = client.getServerByName(connector['host'])
+ chap_secret = server_info['chapTargetSecret']
+ if not chap_enabled and chap_secret:
+ LOG.warning(_LW('CHAP secret exists for host %s but CHAP is '
+ 'disabled'), connector['host'])
+ if chap_enabled and chap_secret is None:
+ LOG.warning(_LW('CHAP is enabled, but server secret not '
+ 'configured on server %s'), connector['host'])
+ return server_info
+ except hpexceptions.HTTPNotFound:
+ # server does not exist, so create one
+ pass
+
+ optional = None
+ if chap_enabled:
+ chap_secret = utils.generate_password()
+ optional = {'chapName': connector['initiator'],
+ 'chapTargetSecret': chap_secret,
+ 'chapAuthenticationRequired': True
+ }
+
+ server_info = client.createServer(connector['host'],
+ connector['initiator'],
+ optional)
+ return server_info
def create_export(self, context, volume, connector):
- return self.proxy.create_export(context, volume, connector)
+ pass
def ensure_export(self, context, volume):
- return self.proxy.ensure_export(context, volume)
+ pass
def remove_export(self, context, volume):
- return self.proxy.remove_export(context, volume)
+ pass
- def retype(self, context, volume, new_type, diff, host):
- """Convert the volume to be of the new type."""
- return self.proxy.retype(context, volume, new_type, diff, host)
+ def retype(self, ctxt, volume, new_type, diff, host):
+ """Convert the volume to be of the new type.
+
+ Returns a boolean indicating whether the retype occurred.
+
+ :param ctxt: Context
+ :param volume: A dictionary describing the volume to retype
+ :param new_type: A dictionary describing the volume type to convert to
+ :param diff: A dictionary with the difference between the two types
+ :param host: A dictionary describing the host, where
+ host['host'] is its name, and host['capabilities'] is a
+ dictionary of its reported capabilities.
+ """
+ LOG.debug('enter: retype: id=%(id)s, new_type=%(new_type)s,'
+ 'diff=%(diff)s, host=%(host)s', {'id': volume['id'],
+ 'new_type': new_type,
+ 'diff': diff,
+ 'host': host})
+ client = self._login()
+ try:
+ volume_info = client.getVolumeByName(volume['name'])
+
+ # pick out the LH extra specs
+ new_extra_specs = dict(new_type).get('extra_specs')
+ lh_extra_specs = self._get_lh_extra_specs(
+ new_extra_specs,
+ extra_specs_key_map.keys())
+
+ LOG.debug('LH specs=%(specs)s', {'specs': lh_extra_specs})
+
+ # only set the ones that have changed
+ changed_extra_specs = {}
+ for key, value in lh_extra_specs.items():
+ (old, new) = diff['extra_specs'][key]
+ if old != new:
+ changed_extra_specs[key] = value
+
+ # map extra specs to LeftHand options
+ options = self._map_extra_specs(changed_extra_specs)
+ if len(options) > 0:
+ client.modifyVolume(volume_info['id'], options)
+ return True
+ except hpexceptions.HTTPNotFound:
+ raise exception.VolumeNotFound(volume_id=volume['id'])
+ except Exception as ex:
+ LOG.warning(_LW("%s"), ex)
+ finally:
+ self._logout(client)
+
+ return False
def migrate_volume(self, ctxt, volume, host):
- """Migrate directly if source and dest are managed by same storage."""
- return self.proxy.migrate_volume(ctxt, volume, host)
+ """Migrate the volume to the specified host.
+
+ Backend assisted volume migration will occur if and only if;
+
+ 1. Same LeftHand backend
+ 2. Volume cannot be attached
+ 3. Volumes with snapshots cannot be migrated
+ 4. Source and Destination clusters must be in the same management group
+
+ Volume re-type is not supported.
+
+ Returns a boolean indicating whether the migration occurred, as well as
+ model_update.
+
+ :param ctxt: Context
+ :param volume: A dictionary describing the volume to migrate
+ :param host: A dictionary describing the host to migrate to, where
+ host['host'] is its name, and host['capabilities'] is a
+ dictionary of its reported capabilities.
+ """
+ LOG.debug('enter: migrate_volume: id=%(id)s, host=%(host)s, '
+ 'cluster=%(cluster)s', {
+ 'id': volume['id'],
+ 'host': host,
+ 'cluster': self.configuration.hplefthand_clustername})
+
+ false_ret = (False, None)
+ if 'location_info' not in host['capabilities']:
+ return false_ret
+
+ host_location = host['capabilities']['location_info']
+ (driver, cluster, vip) = host_location.split(' ')
+ client = self._login()
+ try:
+ # get the cluster info, if it exists and compare
+ cluster_info = client.getClusterByName(cluster)
+ LOG.debug('Cluster info: %s', cluster_info)
+ virtual_ips = cluster_info['virtualIPAddresses']
+
+ if driver != self.__class__.__name__:
+ LOG.info(_LI("Cannot provide backend assisted migration for "
+ "volume: %s because volume is from a different "
+ "backend."), volume['name'])
+ return false_ret
+ if vip != virtual_ips[0]['ipV4Address']:
+ LOG.info(_LI("Cannot provide backend assisted migration for "
+ "volume: %s because cluster exists in different "
+ "management group."), volume['name'])
+ return false_ret
+
+ except hpexceptions.HTTPNotFound:
+ LOG.info(_LI("Cannot provide backend assisted migration for "
+ "volume: %s because cluster exists in different "
+ "management group."), volume['name'])
+ return false_ret
+ finally:
+ self._logout(client)
+
+ client = self._login()
+ try:
+ volume_info = client.getVolumeByName(volume['name'])
+ LOG.debug('Volume info: %s', volume_info)
+
+ # can't migrate if server is attached
+ if volume_info['iscsiSessions'] is not None:
+ LOG.info(_LI("Cannot provide backend assisted migration "
+ "for volume: %s because the volume has been "
+ "exported."), volume['name'])
+ return false_ret
+
+ # can't migrate if volume has snapshots
+ snap_info = client.getVolume(
+ volume_info['id'],
+ 'fields=snapshots,snapshots[resource[members[name]]]')
+ LOG.debug('Snapshot info: %s', snap_info)
+ if snap_info['snapshots']['resource'] is not None:
+ LOG.info(_LI("Cannot provide backend assisted migration "
+ "for volume: %s because the volume has "
+ "snapshots."), volume['name'])
+ return false_ret
+
+ options = {'clusterName': cluster}
+ client.modifyVolume(volume_info['id'], options)
+ except hpexceptions.HTTPNotFound:
+ LOG.info(_LI("Cannot provide backend assisted migration for "
+ "volume: %s because volume does not exist in this "
+ "management group."), volume['name'])
+ return false_ret
+ except hpexceptions.HTTPServerError as ex:
+ LOG.error(_LE("Exception: %s"), ex)
+ return false_ret
+ finally:
+ self._logout(client)
+
+ return (True, None)
def update_migrated_volume(self, context, volume, new_volume,
original_volume_status):
- return self.proxy.update_migrated_volume(context, volume, new_volume,
- original_volume_status)
+ """Rename the new (temp) volume to it's original name.
+
+
+ This method tries to rename the new volume to it's original
+ name after the migration has completed.
+
+ """
+ LOG.debug("Update volume name for %(id)s.", {'id': new_volume['id']})
+ name_id = None
+ provider_location = None
+ if original_volume_status == 'available':
+ # volume isn't attached and can be updated
+ original_name = CONF.volume_name_template % volume['id']
+ current_name = CONF.volume_name_template % new_volume['id']
+ client = self._login()
+ try:
+ volume_info = client.getVolumeByName(current_name)
+ volumeMods = {'name': original_name}
+ client.modifyVolume(volume_info['id'], volumeMods)
+ LOG.info(_LI("Volume name changed from %(tmp)s to %(orig)s."),
+ {'tmp': current_name, 'orig': original_name})
+ except Exception as e:
+ LOG.error(_LE("Changing the volume name from %(tmp)s to "
+ "%(orig)s failed because %(reason)s."),
+ {'tmp': current_name, 'orig': original_name,
+ 'reason': e})
+ name_id = new_volume['_name_id'] or new_volume['id']
+ provider_location = new_volume['provider_location']
+ finally:
+ self._logout(client)
+ else:
+ # the backend can't change the name.
+ name_id = new_volume['_name_id'] or new_volume['id']
+ provider_location = new_volume['provider_location']
+
+ return {'_name_id': name_id, 'provider_location': provider_location}
def manage_existing(self, volume, existing_ref):
- return self.proxy.manage_existing(volume, existing_ref)
+ """Manage an existing LeftHand volume.
+
+ existing_ref is a dictionary of the form:
+ {'source-name': <name of the virtual volume>}
+ """
+ # Check API Version
+ self._check_api_version()
+
+ target_vol_name = self._get_existing_volume_ref_name(existing_ref)
+
+ # Check for the existence of the virtual volume.
+ client = self._login()
+ try:
+ volume_info = client.getVolumeByName(target_vol_name)
+ except hpexceptions.HTTPNotFound:
+ err = (_("Virtual volume '%s' doesn't exist on array.") %
+ target_vol_name)
+ LOG.error(err)
+ raise exception.InvalidInput(reason=err)
+ finally:
+ self._logout(client)
+
+ # Generate the new volume information based on the new ID.
+ new_vol_name = 'volume-' + volume['id']
+
+ volume_type = None
+ if volume['volume_type_id']:
+ try:
+ volume_type = self._get_volume_type(volume['volume_type_id'])
+ except Exception:
+ reason = (_("Volume type ID '%s' is invalid.") %
+ volume['volume_type_id'])
+ raise exception.ManageExistingVolumeTypeMismatch(reason=reason)
+
+ new_vals = {"name": new_vol_name}
+
+ client = self._login()
+ try:
+ # Update the existing volume with the new name.
+ client.modifyVolume(volume_info['id'], new_vals)
+ finally:
+ self._logout(client)
+
+ LOG.info(_LI("Virtual volume '%(ref)s' renamed to '%(new)s'."),
+ {'ref': existing_ref['source-name'], 'new': new_vol_name})
+
+ display_name = None
+ if volume['display_name']:
+ display_name = volume['display_name']
+
+ if volume_type:
+ LOG.info(_LI("Virtual volume %(disp)s '%(new)s' is "
+ "being retyped."),
+ {'disp': display_name, 'new': new_vol_name})
+
+ try:
+ self.retype(None,
+ volume,
+ volume_type,
+ volume_type['extra_specs'],
+ volume['host'])
+ LOG.info(_LI("Virtual volume %(disp)s successfully retyped to "
+ "%(new_type)s."),
+ {'disp': display_name,
+ 'new_type': volume_type.get('name')})
+ except Exception:
+ with excutils.save_and_reraise_exception():
+ LOG.warning(_LW("Failed to manage virtual volume %(disp)s "
+ "due to error during retype."),
+ {'disp': display_name})
+ # Try to undo the rename and clear the new comment.
+ client = self._login()
+ try:
+ client.modifyVolume(
+ volume_info['id'],
+ {'name': target_vol_name})
+ finally:
+ self._logout(client)
+
+ updates = {'display_name': display_name}
+
+ LOG.info(_LI("Virtual volume %(disp)s '%(new)s' is "
+ "now being managed."),
+ {'disp': display_name, 'new': new_vol_name})
+
+ # Return display name to update the name displayed in the GUI and
+ # any model updates from retype.
+ return updates
def manage_existing_get_size(self, volume, existing_ref):
- return self.proxy.manage_existing_get_size(volume, existing_ref)
+ """Return size of volume to be managed by manage_existing.
+
+ existing_ref is a dictionary of the form:
+ {'source-name': <name of the virtual volume>}
+ """
+ # Check API version.
+ self._check_api_version()
+
+ target_vol_name = self._get_existing_volume_ref_name(existing_ref)
+
+ # Make sure the reference is not in use.
+ if re.match('volume-*|snapshot-*', target_vol_name):
+ reason = _("Reference must be the volume name of an unmanaged "
+ "virtual volume.")
+ raise exception.ManageExistingInvalidReference(
+ existing_ref=target_vol_name,
+ reason=reason)
+
+ # Check for the existence of the virtual volume.
+ client = self._login()
+ try:
+ volume_info = client.getVolumeByName(target_vol_name)
+ except hpexceptions.HTTPNotFound:
+ err = (_("Virtual volume '%s' doesn't exist on array.") %
+ target_vol_name)
+ LOG.error(err)
+ raise exception.InvalidInput(reason=err)
+ finally:
+ self._logout(client)
+
+ return int(math.ceil(float(volume_info['size']) / units.Gi))
def unmanage(self, volume):
- return self.proxy.unmanage(volume)
+ """Removes the specified volume from Cinder management."""
+ # Check API version.
+ self._check_api_version()
+
+ # Rename the volume's name to unm-* format so that it can be
+ # easily found later.
+ client = self._login()
+ try:
+ volume_info = client.getVolumeByName(volume['name'])
+ new_vol_name = 'unm-' + six.text_type(volume['id'])
+ options = {'name': new_vol_name}
+ client.modifyVolume(volume_info['id'], options)
+ finally:
+ self._logout(client)
+
+ LOG.info(_LI("Virtual volume %(disp)s '%(vol)s' is no longer managed. "
+ "Volume renamed to '%(new)s'."),
+ {'disp': volume['display_name'],
+ 'vol': volume['name'],
+ 'new': new_vol_name})
+
+ def _get_existing_volume_ref_name(self, existing_ref):
+ """Returns the volume name of an existing reference.
+
+ Checks if an existing volume reference has a source-name element.
+ If source-name is not present an error will be thrown.
+ """
+ if 'source-name' not in existing_ref:
+ reason = _("Reference must contain source-name.")
+ raise exception.ManageExistingInvalidReference(
+ existing_ref=existing_ref,
+ reason=reason)
+
+ return existing_ref['source-name']
+
+ def _check_api_version(self):
+ """Checks that the API version is correct."""
+ if (self.api_version < MIN_API_VERSION):
+ ex_msg = (_('Invalid HPLeftHand API version found: %(found)s. '
+ 'Version %(minimum)s or greater required for '
+ 'manage/unmanage support.')
+ % {'found': self.api_version,
+ 'minimum': MIN_API_VERSION})
+ LOG.error(ex_msg)
+ raise exception.InvalidInput(reason=ex_msg)
+
+ def _get_volume_type(self, type_id):
+ ctxt = context.get_admin_context()
+ return volume_types.get_volume_type(ctxt, type_id)
+++ /dev/null
-# (c) Copyright 2014-2015 Hewlett-Packard Development Company, L.P.
-# All Rights Reserved.
-#
-# 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.
-#
-"""HP LeftHand SAN ISCSI REST Proxy."""
-
-from oslo_config import cfg
-from oslo_log import log as logging
-from oslo_utils import excutils
-from oslo_utils import importutils
-from oslo_utils import units
-
-from cinder import context
-from cinder import exception
-from cinder.i18n import _, _LE, _LI, _LW
-from cinder import objects
-from cinder.volume import driver
-from cinder.volume import utils
-from cinder.volume import volume_types
-
-import six
-
-import math
-import re
-
-LOG = logging.getLogger(__name__)
-
-hplefthandclient = importutils.try_import("hplefthandclient")
-if hplefthandclient:
- from hplefthandclient import client as hp_lh_client
- from hplefthandclient import exceptions as hpexceptions
-
-hplefthand_opts = [
- cfg.StrOpt('hplefthand_api_url',
- help="HP LeftHand WSAPI Server Url like "
- "https://<LeftHand ip>:8081/lhos"),
- cfg.StrOpt('hplefthand_username',
- help="HP LeftHand Super user username"),
- cfg.StrOpt('hplefthand_password',
- help="HP LeftHand Super user password",
- secret=True),
- cfg.StrOpt('hplefthand_clustername',
- help="HP LeftHand cluster name"),
- cfg.BoolOpt('hplefthand_iscsi_chap_enabled',
- default=False,
- help='Configure CHAP authentication for iSCSI connections '
- '(Default: Disabled)'),
- cfg.BoolOpt('hplefthand_debug',
- default=False,
- help="Enable HTTP debugging to LeftHand"),
-
-]
-
-CONF = cfg.CONF
-CONF.register_opts(hplefthand_opts)
-
-MIN_API_VERSION = "1.1"
-MIN_CG_CLIENT_VERSION = "1.0.6"
-
-# map the extra spec key to the REST client option key
-extra_specs_key_map = {
- 'hplh:provisioning': 'isThinProvisioned',
- 'hplh:ao': 'isAdaptiveOptimizationEnabled',
- 'hplh:data_pl': 'dataProtectionLevel',
-}
-
-# map the extra spec value to the REST client option value
-extra_specs_value_map = {
- 'isThinProvisioned': {'thin': True, 'full': False},
- 'isAdaptiveOptimizationEnabled': {'true': True, 'false': False},
- 'dataProtectionLevel': {
- 'r-0': 0, 'r-5': 1, 'r-10-2': 2, 'r-10-3': 3, 'r-10-4': 4, 'r-6': 5}
-}
-
-
-class HPLeftHandRESTProxy(driver.ISCSIDriver):
- """Executes REST commands relating to HP/LeftHand SAN ISCSI volumes.
-
- Version history:
- 1.0.0 - Initial REST iSCSI proxy
- 1.0.1 - Added support for retype
- 1.0.2 - Added support for volume migrate
- 1.0.3 - Fixed bug #1285829, HP LeftHand backend assisted migration
- should check for snapshots
- 1.0.4 - Fixed bug #1285925, LeftHand AO volume create performance
- improvement
- 1.0.5 - Fixed bug #1311350, Live-migration of an instance when
- attached to a volume was causing an error.
- 1.0.6 - Removing locks bug #1395953
- 1.0.7 - Fixed bug #1353137, Server was not removed from the HP
- Lefthand backend after the last volume was detached.
- 1.0.8 - Fixed bug #1418201, A cloned volume fails to attach.
- 1.0.9 - Adding support for manage/unmanage.
- 1.0.10 - Add stats for goodness_function and filter_function
- 1.0.11 - Add over subscription support
- 1.0.12 - Adds consistency group support
- 1.0.13 - Added update_migrated_volume #1493546
- """
-
- VERSION = "1.0.13"
-
- device_stats = {}
-
- def __init__(self, *args, **kwargs):
- super(HPLeftHandRESTProxy, self).__init__(*args, **kwargs)
- self.configuration.append_config_values(hplefthand_opts)
- if not self.configuration.hplefthand_api_url:
- raise exception.NotFound(_("HPLeftHand url not found"))
-
- # blank is the only invalid character for cluster names
- # so we need to use it as a separator
- self.DRIVER_LOCATION = self.__class__.__name__ + ' %(cluster)s %(vip)s'
- self.db = kwargs.get('db')
-
- def _login(self):
- client = self.do_setup(None)
- return client
-
- def _logout(self, client):
- client.logout()
-
- def _create_client(self):
- return hp_lh_client.HPLeftHandClient(
- self.configuration.hplefthand_api_url)
-
- def do_setup(self, context):
- """Set up LeftHand client."""
- try:
- client = self._create_client()
- client.login(
- self.configuration.hplefthand_username,
- self.configuration.hplefthand_password)
-
- if self.configuration.hplefthand_debug:
- client.debug_rest(True)
-
- cluster_info = client.getClusterByName(
- self.configuration.hplefthand_clustername)
- self.cluster_id = cluster_info['id']
- virtual_ips = cluster_info['virtualIPAddresses']
- self.cluster_vip = virtual_ips[0]['ipV4Address']
- self._update_backend_status(client)
-
- return client
- except hpexceptions.HTTPNotFound:
- raise exception.DriverNotInitialized(
- _('LeftHand cluster not found'))
- except Exception as ex:
- raise exception.DriverNotInitialized(ex)
-
- def check_for_setup_error(self):
- """Checks for incorrect LeftHand API being used on backend."""
- client = self._login()
- try:
- self.api_version = client.getApiVersion()
-
- LOG.info(_LI("HPLeftHand API version %s"), self.api_version)
-
- if self.api_version < MIN_API_VERSION:
- LOG.warning(_LW("HPLeftHand API is version %(current)s. "
- "A minimum version of %(min)s is needed for "
- "manage/unmanage support."),
- {'current': self.api_version,
- 'min': MIN_API_VERSION})
- finally:
- self._logout(client)
-
- def get_version_string(self):
- return (_('REST %(proxy_ver)s hplefthandclient %(rest_ver)s') % {
- 'proxy_ver': self.VERSION,
- 'rest_ver': hplefthandclient.get_version_string()})
-
- def create_volume(self, volume):
- """Creates a volume."""
- client = self._login()
- try:
- # get the extra specs of interest from this volume's volume type
- volume_extra_specs = self._get_volume_extra_specs(volume)
- extra_specs = self._get_lh_extra_specs(
- volume_extra_specs,
- extra_specs_key_map.keys())
-
- # map the extra specs key/value pairs to key/value pairs
- # used as optional configuration values by the LeftHand backend
- optional = self._map_extra_specs(extra_specs)
-
- # if provisioning is not set, default to thin
- if 'isThinProvisioned' not in optional:
- optional['isThinProvisioned'] = True
-
- # AdaptiveOptimization defaults to 'true' if you don't specify the
- # value on a create, and that is the most efficient way to create
- # a volume. If you pass in 'false' or 'true' for AO, it will result
- # in an update operation following the create operation to set this
- # value, so it is best to not specify the value and let it default
- # to 'true'.
- if optional.get('isAdaptiveOptimizationEnabled'):
- del optional['isAdaptiveOptimizationEnabled']
-
- clusterName = self.configuration.hplefthand_clustername
- optional['clusterName'] = clusterName
-
- volume_info = client.createVolume(
- volume['name'], self.cluster_id,
- volume['size'] * units.Gi,
- optional)
-
- return self._update_provider(volume_info)
- except Exception as ex:
- raise exception.VolumeBackendAPIException(ex)
- finally:
- self._logout(client)
-
- def delete_volume(self, volume):
- """Deletes a volume."""
- client = self._login()
- try:
- volume_info = client.getVolumeByName(volume['name'])
- client.deleteVolume(volume_info['id'])
- except hpexceptions.HTTPNotFound:
- LOG.error(_LE("Volume did not exist. It will not be deleted"))
- except Exception as ex:
- raise exception.VolumeBackendAPIException(ex)
- finally:
- self._logout(client)
-
- def extend_volume(self, volume, new_size):
- """Extend the size of an existing volume."""
- client = self._login()
- try:
- volume_info = client.getVolumeByName(volume['name'])
-
- # convert GB to bytes
- options = {'size': int(new_size) * units.Gi}
- client.modifyVolume(volume_info['id'], options)
- except Exception as ex:
- raise exception.VolumeBackendAPIException(ex)
- finally:
- self._logout(client)
-
- def create_consistencygroup(self, context, group):
- """Creates a consistencygroup."""
- model_update = {'status': 'available'}
- return model_update
-
- def create_consistencygroup_from_src(self, context, group, volumes,
- cgsnapshot=None, snapshots=None,
- source_cg=None, source_vols=None):
- """Creates a consistency group from a source"""
- LOG.error(_LE("Creating a consistency group from a source is not "
- "currently supported."))
- raise NotImplementedError()
-
- def delete_consistencygroup(self, context, group):
- """Deletes a consistency group."""
- # TODO(aorourke): Can't eliminate the DB calls here due to CG API.
- # Will fix in M release
- volumes = self.db.volume_get_all_by_group(context, group.id)
- for volume in volumes:
- self.delete_volume(volume)
- volume.status = 'deleted'
-
- model_update = {'status': group.status}
-
- return model_update, volumes
-
- def update_consistencygroup(self, context, group,
- add_volumes=None, remove_volumes=None):
- """Updates a consistency group.
-
- Because the backend has no concept of volume grouping, cinder will
- maintain all volume/consistency group relationships. Because of this
- functionality, there is no need to make any client calls; instead
- simply returning out of this function allows cinder to properly
- add/remove volumes from the consistency group.
- """
- return None, None, None
-
- def create_cgsnapshot(self, context, cgsnapshot):
- """Creates a consistency group snapshot."""
- client = self._login()
- try:
- # TODO(aorourke): Can't eliminate the DB calls here due to CG API.
- # Will fix in M release
- snapshots = objects.SnapshotList().get_all_for_cgsnapshot(
- context, cgsnapshot['id'])
-
- snap_set = []
- snapshot_base_name = "snapshot-" + cgsnapshot['id']
- for i, snapshot in enumerate(snapshots):
- volume = snapshot.volume
- volume_name = volume['name']
- try:
- volume_info = client.getVolumeByName(volume_name)
- except Exception as ex:
- error = six.text_type(ex)
- LOG.error(_LE("Could not find volume with name %(name)s. "
- "Error: %(error)s"),
- {'name': volume_name,
- 'error': error})
- raise exception.VolumeBackendAPIException(data=error)
-
- volume_id = volume_info['id']
- snapshot_name = snapshot_base_name + "-" + six.text_type(i)
- snap_set_member = {'volumeName': volume_name,
- 'volumeId': volume_id,
- 'snapshotName': snapshot_name}
- snap_set.append(snap_set_member)
- snapshot.status = 'available'
-
- source_volume_id = snap_set[0]['volumeId']
- optional = {'inheritAccess': True}
- description = cgsnapshot.get('description', None)
- if description:
- optional['description'] = description
-
- try:
- client.createSnapshotSet(source_volume_id, snap_set, optional)
- except Exception as ex:
- error = six.text_type(ex)
- LOG.error(_LE("Could not create snapshot set. Error: '%s'"),
- error)
- raise exception.VolumeBackendAPIException(
- data=error)
-
- except Exception as ex:
- raise exception.VolumeBackendAPIException(data=six.text_type(ex))
- finally:
- self._logout(client)
-
- model_update = {'status': 'available'}
-
- return model_update, snapshots
-
- def delete_cgsnapshot(self, context, cgsnapshot):
- """Deletes a consistency group snapshot."""
-
- client = self._login()
- try:
- snap_name_base = "snapshot-" + cgsnapshot['id']
-
- # TODO(aorourke): Can't eliminate the DB calls here due to CG API.
- # Will fix in M release
- snapshots = objects.SnapshotList().get_all_for_cgsnapshot(
- context, cgsnapshot['id'])
-
- for i, snapshot in enumerate(snapshots):
- try:
- snap_name = snap_name_base + "-" + six.text_type(i)
- snap_info = client.getSnapshotByName(snap_name)
- client.deleteSnapshot(snap_info['id'])
- except hpexceptions.HTTPNotFound:
- LOG.error(_LE("Snapshot did not exist. It will not be "
- "deleted."))
- except hpexceptions.HTTPServerError as ex:
- in_use_msg = ('cannot be deleted because it is a clone '
- 'point')
- if in_use_msg in ex.get_description():
- raise exception.SnapshotIsBusy(snapshot_name=snap_name)
-
- raise exception.VolumeBackendAPIException(
- data=six.text_type(ex))
-
- except Exception as ex:
- raise exception.VolumeBackendAPIException(
- data=six.text_type(ex))
- finally:
- self._logout(client)
-
- model_update = {'status': cgsnapshot['status']}
-
- return model_update, snapshots
-
- def create_snapshot(self, snapshot):
- """Creates a snapshot."""
- client = self._login()
- try:
- volume_info = client.getVolumeByName(snapshot['volume_name'])
-
- option = {'inheritAccess': True}
- client.createSnapshot(snapshot['name'],
- volume_info['id'],
- option)
- except Exception as ex:
- raise exception.VolumeBackendAPIException(ex)
- finally:
- self._logout(client)
-
- def delete_snapshot(self, snapshot):
- """Deletes a snapshot."""
- client = self._login()
- try:
- snap_info = client.getSnapshotByName(snapshot['name'])
- client.deleteSnapshot(snap_info['id'])
- except hpexceptions.HTTPNotFound:
- LOG.error(_LE("Snapshot did not exist. It will not be deleted"))
- except hpexceptions.HTTPServerError as ex:
- in_use_msg = 'cannot be deleted because it is a clone point'
- if in_use_msg in ex.get_description():
- raise exception.SnapshotIsBusy(ex)
-
- raise exception.VolumeBackendAPIException(ex)
-
- except Exception as ex:
- raise exception.VolumeBackendAPIException(ex)
- finally:
- self._logout(client)
-
- def get_volume_stats(self, refresh=False):
- """Gets volume stats."""
- client = self._login()
- try:
- if refresh:
- self._update_backend_status(client)
-
- return self.device_stats
- finally:
- self._logout(client)
-
- def _update_backend_status(self, client):
- data = {}
- backend_name = self.configuration.safe_get('volume_backend_name')
- data['volume_backend_name'] = backend_name or self.__class__.__name__
- data['reserved_percentage'] = (
- self.configuration.safe_get('reserved_percentage'))
- data['storage_protocol'] = 'iSCSI'
- data['vendor_name'] = 'Hewlett-Packard'
- data['location_info'] = (self.DRIVER_LOCATION % {
- 'cluster': self.configuration.hplefthand_clustername,
- 'vip': self.cluster_vip})
- data['thin_provisioning_support'] = True
- data['thick_provisioning_support'] = True
- data['max_over_subscription_ratio'] = (
- self.configuration.safe_get('max_over_subscription_ratio'))
-
- cluster_info = client.getCluster(self.cluster_id)
-
- total_capacity = cluster_info['spaceTotal']
- free_capacity = cluster_info['spaceAvailable']
-
- # convert to GB
- data['total_capacity_gb'] = int(total_capacity) / units.Gi
- data['free_capacity_gb'] = int(free_capacity) / units.Gi
-
- # Collect some stats
- capacity_utilization = (
- (float(total_capacity - free_capacity) /
- float(total_capacity)) * 100)
- # Don't have a better way to get the total number volumes
- # so try to limit the size of data for now. Once new lefthand API is
- # available, replace this call.
- total_volumes = 0
- provisioned_size = 0
- volumes = client.getVolumes(
- cluster=self.configuration.hplefthand_clustername,
- fields=['members[id]', 'members[clusterName]', 'members[size]'])
- if volumes:
- total_volumes = volumes['total']
- provisioned_size = sum(
- members['size'] for members in volumes['members'])
- data['provisioned_capacity_gb'] = int(provisioned_size) / units.Gi
- data['capacity_utilization'] = capacity_utilization
- data['total_volumes'] = total_volumes
- data['filter_function'] = self.get_filter_function()
- data['goodness_function'] = self.get_goodness_function()
- if hplefthandclient.version >= MIN_CG_CLIENT_VERSION:
- data['consistencygroup_support'] = True
-
- self.device_stats = data
-
- def initialize_connection(self, volume, connector):
- """Assigns the volume to a server.
-
- Assign any created volume to a compute node/host so that it can be
- used from that host. HP VSA requires a volume to be assigned
- to a server.
- """
- client = self._login()
- try:
- server_info = self._create_server(connector, client)
- volume_info = client.getVolumeByName(volume['name'])
-
- access_already_enabled = False
- if volume_info['iscsiSessions'] is not None:
- # Extract the server id for each session to check if the
- # new server already has access permissions enabled.
- for session in volume_info['iscsiSessions']:
- server_id = int(session['server']['uri'].split('/')[3])
- if server_id == server_info['id']:
- access_already_enabled = True
- break
-
- if not access_already_enabled:
- client.addServerAccess(
- volume_info['id'],
- server_info['id'])
-
- iscsi_properties = self._get_iscsi_properties(volume)
-
- if ('chapAuthenticationRequired' in server_info and
- server_info['chapAuthenticationRequired']):
- iscsi_properties['auth_method'] = 'CHAP'
- iscsi_properties['auth_username'] = connector['initiator']
- iscsi_properties['auth_password'] = (
- server_info['chapTargetSecret'])
-
- return {'driver_volume_type': 'iscsi', 'data': iscsi_properties}
- except Exception as ex:
- raise exception.VolumeBackendAPIException(ex)
- finally:
- self._logout(client)
-
- def terminate_connection(self, volume, connector, **kwargs):
- """Unassign the volume from the host."""
- client = self._login()
- try:
- volume_info = client.getVolumeByName(volume['name'])
- server_info = client.getServerByName(connector['host'])
- volume_list = client.findServerVolumes(server_info['name'])
-
- removeServer = True
- for entry in volume_list:
- if entry['id'] != volume_info['id']:
- removeServer = False
- break
-
- client.removeServerAccess(
- volume_info['id'],
- server_info['id'])
-
- if removeServer:
- client.deleteServer(server_info['id'])
- except Exception as ex:
- raise exception.VolumeBackendAPIException(ex)
- finally:
- self._logout(client)
-
- def create_volume_from_snapshot(self, volume, snapshot):
- """Creates a volume from a snapshot."""
- client = self._login()
- try:
- snap_info = client.getSnapshotByName(snapshot['name'])
- volume_info = client.cloneSnapshot(
- volume['name'],
- snap_info['id'])
- return self._update_provider(volume_info)
- except Exception as ex:
- raise exception.VolumeBackendAPIException(ex)
- finally:
- self._logout(client)
-
- def create_cloned_volume(self, volume, src_vref):
- client = self._login()
- try:
- volume_info = client.getVolumeByName(src_vref['name'])
- clone_info = client.cloneVolume(volume['name'], volume_info['id'])
- return self._update_provider(clone_info)
- except Exception as ex:
- raise exception.VolumeBackendAPIException(ex)
- finally:
- self._logout(client)
-
- def _get_volume_extra_specs(self, volume):
- """Get extra specs from a volume."""
- extra_specs = {}
- type_id = volume.get('volume_type_id', None)
- if type_id is not None:
- ctxt = context.get_admin_context()
- volume_type = volume_types.get_volume_type(ctxt, type_id)
- extra_specs = volume_type.get('extra_specs')
- return extra_specs
-
- def _get_lh_extra_specs(self, extra_specs, valid_keys):
- """Get LeftHand extra_specs (valid_keys only)."""
- extra_specs_of_interest = {}
- for key, value in extra_specs.items():
- if key in valid_keys:
- extra_specs_of_interest[key] = value
- return extra_specs_of_interest
-
- def _map_extra_specs(self, extra_specs):
- """Map the extra spec key/values to LeftHand key/values."""
- client_options = {}
- for key, value in extra_specs.items():
- # map extra spec key to lh client option key
- client_key = extra_specs_key_map[key]
- # map extra spect value to lh client option value
- try:
- value_map = extra_specs_value_map[client_key]
- # an invalid value will throw KeyError
- client_value = value_map[value]
- client_options[client_key] = client_value
- except KeyError:
- LOG.error(_LE("'%(value)s' is an invalid value "
- "for extra spec '%(key)s'"),
- {'value': value, 'key': key})
- return client_options
-
- def _update_provider(self, volume_info):
- # TODO(justinsb): Is this always 1? Does it matter?
- cluster_interface = '1'
- iscsi_portal = self.cluster_vip + ":3260," + cluster_interface
-
- return {'provider_location': (
- "%s %s %s" % (iscsi_portal, volume_info['iscsiIqn'], 0))}
-
- def _create_server(self, connector, client):
- server_info = None
- chap_enabled = self.configuration.hplefthand_iscsi_chap_enabled
- try:
- server_info = client.getServerByName(connector['host'])
- chap_secret = server_info['chapTargetSecret']
- if not chap_enabled and chap_secret:
- LOG.warning(_LW('CHAP secret exists for host %s but CHAP is '
- 'disabled'), connector['host'])
- if chap_enabled and chap_secret is None:
- LOG.warning(_LW('CHAP is enabled, but server secret not '
- 'configured on server %s'), connector['host'])
- return server_info
- except hpexceptions.HTTPNotFound:
- # server does not exist, so create one
- pass
-
- optional = None
- if chap_enabled:
- chap_secret = utils.generate_password()
- optional = {'chapName': connector['initiator'],
- 'chapTargetSecret': chap_secret,
- 'chapAuthenticationRequired': True
- }
-
- server_info = client.createServer(connector['host'],
- connector['initiator'],
- optional)
- return server_info
-
- def create_export(self, context, volume, connector):
- pass
-
- def ensure_export(self, context, volume):
- pass
-
- def remove_export(self, context, volume):
- pass
-
- def retype(self, ctxt, volume, new_type, diff, host):
- """Convert the volume to be of the new type.
-
- Returns a boolean indicating whether the retype occurred.
-
- :param ctxt: Context
- :param volume: A dictionary describing the volume to retype
- :param new_type: A dictionary describing the volume type to convert to
- :param diff: A dictionary with the difference between the two types
- :param host: A dictionary describing the host, where
- host['host'] is its name, and host['capabilities'] is a
- dictionary of its reported capabilities.
- """
- LOG.debug('enter: retype: id=%(id)s, new_type=%(new_type)s,'
- 'diff=%(diff)s, host=%(host)s', {'id': volume['id'],
- 'new_type': new_type,
- 'diff': diff,
- 'host': host})
- client = self._login()
- try:
- volume_info = client.getVolumeByName(volume['name'])
-
- # pick out the LH extra specs
- new_extra_specs = dict(new_type).get('extra_specs')
- lh_extra_specs = self._get_lh_extra_specs(
- new_extra_specs,
- extra_specs_key_map.keys())
-
- LOG.debug('LH specs=%(specs)s', {'specs': lh_extra_specs})
-
- # only set the ones that have changed
- changed_extra_specs = {}
- for key, value in lh_extra_specs.items():
- (old, new) = diff['extra_specs'][key]
- if old != new:
- changed_extra_specs[key] = value
-
- # map extra specs to LeftHand options
- options = self._map_extra_specs(changed_extra_specs)
- if len(options) > 0:
- client.modifyVolume(volume_info['id'], options)
- return True
- except hpexceptions.HTTPNotFound:
- raise exception.VolumeNotFound(volume_id=volume['id'])
- except Exception as ex:
- LOG.warning(_LW("%s"), ex)
- finally:
- self._logout(client)
-
- return False
-
- def migrate_volume(self, ctxt, volume, host):
- """Migrate the volume to the specified host.
-
- Backend assisted volume migration will occur if and only if;
-
- 1. Same LeftHand backend
- 2. Volume cannot be attached
- 3. Volumes with snapshots cannot be migrated
- 4. Source and Destination clusters must be in the same management group
-
- Volume re-type is not supported.
-
- Returns a boolean indicating whether the migration occurred, as well as
- model_update.
-
- :param ctxt: Context
- :param volume: A dictionary describing the volume to migrate
- :param host: A dictionary describing the host to migrate to, where
- host['host'] is its name, and host['capabilities'] is a
- dictionary of its reported capabilities.
- """
- LOG.debug('enter: migrate_volume: id=%(id)s, host=%(host)s, '
- 'cluster=%(cluster)s', {
- 'id': volume['id'],
- 'host': host,
- 'cluster': self.configuration.hplefthand_clustername})
-
- false_ret = (False, None)
- if 'location_info' not in host['capabilities']:
- return false_ret
-
- host_location = host['capabilities']['location_info']
- (driver, cluster, vip) = host_location.split(' ')
- client = self._login()
- try:
- # get the cluster info, if it exists and compare
- cluster_info = client.getClusterByName(cluster)
- LOG.debug('Cluster info: %s', cluster_info)
- virtual_ips = cluster_info['virtualIPAddresses']
-
- if driver != self.__class__.__name__:
- LOG.info(_LI("Cannot provide backend assisted migration for "
- "volume: %s because volume is from a different "
- "backend."), volume['name'])
- return false_ret
- if vip != virtual_ips[0]['ipV4Address']:
- LOG.info(_LI("Cannot provide backend assisted migration for "
- "volume: %s because cluster exists in different "
- "management group."), volume['name'])
- return false_ret
-
- except hpexceptions.HTTPNotFound:
- LOG.info(_LI("Cannot provide backend assisted migration for "
- "volume: %s because cluster exists in different "
- "management group."), volume['name'])
- return false_ret
- finally:
- self._logout(client)
-
- client = self._login()
- try:
- volume_info = client.getVolumeByName(volume['name'])
- LOG.debug('Volume info: %s', volume_info)
-
- # can't migrate if server is attached
- if volume_info['iscsiSessions'] is not None:
- LOG.info(_LI("Cannot provide backend assisted migration "
- "for volume: %s because the volume has been "
- "exported."), volume['name'])
- return false_ret
-
- # can't migrate if volume has snapshots
- snap_info = client.getVolume(
- volume_info['id'],
- 'fields=snapshots,snapshots[resource[members[name]]]')
- LOG.debug('Snapshot info: %s', snap_info)
- if snap_info['snapshots']['resource'] is not None:
- LOG.info(_LI("Cannot provide backend assisted migration "
- "for volume: %s because the volume has "
- "snapshots."), volume['name'])
- return false_ret
-
- options = {'clusterName': cluster}
- client.modifyVolume(volume_info['id'], options)
- except hpexceptions.HTTPNotFound:
- LOG.info(_LI("Cannot provide backend assisted migration for "
- "volume: %s because volume does not exist in this "
- "management group."), volume['name'])
- return false_ret
- except hpexceptions.HTTPServerError as ex:
- LOG.error(_LE("Exception: %s"), ex)
- return false_ret
- finally:
- self._logout(client)
-
- return (True, None)
-
- def update_migrated_volume(self, context, volume, new_volume,
- original_volume_status):
- """Rename the new (temp) volume to it's original name.
-
-
- This method tries to rename the new volume to it's original
- name after the migration has completed.
-
- """
- LOG.debug("Update volume name for %(id)s.", {'id': new_volume['id']})
- name_id = None
- provider_location = None
- if original_volume_status == 'available':
- # volume isn't attached and can be updated
- original_name = CONF.volume_name_template % volume['id']
- current_name = CONF.volume_name_template % new_volume['id']
- client = self._login()
- try:
- volume_info = client.getVolumeByName(current_name)
- volumeMods = {'name': original_name}
- client.modifyVolume(volume_info['id'], volumeMods)
- LOG.info(_LI("Volume name changed from %(tmp)s to %(orig)s."),
- {'tmp': current_name, 'orig': original_name})
- except Exception as e:
- LOG.error(_LE("Changing the volume name from %(tmp)s to "
- "%(orig)s failed because %(reason)s."),
- {'tmp': current_name, 'orig': original_name,
- 'reason': e})
- name_id = new_volume['_name_id'] or new_volume['id']
- provider_location = new_volume['provider_location']
- finally:
- self._logout(client)
- else:
- # the backend can't change the name.
- name_id = new_volume['_name_id'] or new_volume['id']
- provider_location = new_volume['provider_location']
-
- return {'_name_id': name_id, 'provider_location': provider_location}
-
- def manage_existing(self, volume, existing_ref):
- """Manage an existing LeftHand volume.
-
- existing_ref is a dictionary of the form:
- {'source-name': <name of the virtual volume>}
- """
- # Check API Version
- self._check_api_version()
-
- target_vol_name = self._get_existing_volume_ref_name(existing_ref)
-
- # Check for the existence of the virtual volume.
- client = self._login()
- try:
- volume_info = client.getVolumeByName(target_vol_name)
- except hpexceptions.HTTPNotFound:
- err = (_("Virtual volume '%s' doesn't exist on array.") %
- target_vol_name)
- LOG.error(err)
- raise exception.InvalidInput(reason=err)
- finally:
- self._logout(client)
-
- # Generate the new volume information based on the new ID.
- new_vol_name = 'volume-' + volume['id']
-
- volume_type = None
- if volume['volume_type_id']:
- try:
- volume_type = self._get_volume_type(volume['volume_type_id'])
- except Exception:
- reason = (_("Volume type ID '%s' is invalid.") %
- volume['volume_type_id'])
- raise exception.ManageExistingVolumeTypeMismatch(reason=reason)
-
- new_vals = {"name": new_vol_name}
-
- client = self._login()
- try:
- # Update the existing volume with the new name.
- client.modifyVolume(volume_info['id'], new_vals)
- finally:
- self._logout(client)
-
- LOG.info(_LI("Virtual volume '%(ref)s' renamed to '%(new)s'."),
- {'ref': existing_ref['source-name'], 'new': new_vol_name})
-
- display_name = None
- if volume['display_name']:
- display_name = volume['display_name']
-
- if volume_type:
- LOG.info(_LI("Virtual volume %(disp)s '%(new)s' is "
- "being retyped."),
- {'disp': display_name, 'new': new_vol_name})
-
- try:
- self.retype(None,
- volume,
- volume_type,
- volume_type['extra_specs'],
- volume['host'])
- LOG.info(_LI("Virtual volume %(disp)s successfully retyped to "
- "%(new_type)s."),
- {'disp': display_name,
- 'new_type': volume_type.get('name')})
- except Exception:
- with excutils.save_and_reraise_exception():
- LOG.warning(_LW("Failed to manage virtual volume %(disp)s "
- "due to error during retype."),
- {'disp': display_name})
- # Try to undo the rename and clear the new comment.
- client = self._login()
- try:
- client.modifyVolume(
- volume_info['id'],
- {'name': target_vol_name})
- finally:
- self._logout(client)
-
- updates = {'display_name': display_name}
-
- LOG.info(_LI("Virtual volume %(disp)s '%(new)s' is "
- "now being managed."),
- {'disp': display_name, 'new': new_vol_name})
-
- # Return display name to update the name displayed in the GUI and
- # any model updates from retype.
- return updates
-
- def manage_existing_get_size(self, volume, existing_ref):
- """Return size of volume to be managed by manage_existing.
-
- existing_ref is a dictionary of the form:
- {'source-name': <name of the virtual volume>}
- """
- # Check API version.
- self._check_api_version()
-
- target_vol_name = self._get_existing_volume_ref_name(existing_ref)
-
- # Make sure the reference is not in use.
- if re.match('volume-*|snapshot-*', target_vol_name):
- reason = _("Reference must be the volume name of an unmanaged "
- "virtual volume.")
- raise exception.ManageExistingInvalidReference(
- existing_ref=target_vol_name,
- reason=reason)
-
- # Check for the existence of the virtual volume.
- client = self._login()
- try:
- volume_info = client.getVolumeByName(target_vol_name)
- except hpexceptions.HTTPNotFound:
- err = (_("Virtual volume '%s' doesn't exist on array.") %
- target_vol_name)
- LOG.error(err)
- raise exception.InvalidInput(reason=err)
- finally:
- self._logout(client)
-
- return int(math.ceil(float(volume_info['size']) / units.Gi))
-
- def unmanage(self, volume):
- """Removes the specified volume from Cinder management."""
- # Check API version.
- self._check_api_version()
-
- # Rename the volume's name to unm-* format so that it can be
- # easily found later.
- client = self._login()
- try:
- volume_info = client.getVolumeByName(volume['name'])
- new_vol_name = 'unm-' + six.text_type(volume['id'])
- options = {'name': new_vol_name}
- client.modifyVolume(volume_info['id'], options)
- finally:
- self._logout(client)
-
- LOG.info(_LI("Virtual volume %(disp)s '%(vol)s' is no longer managed. "
- "Volume renamed to '%(new)s'."),
- {'disp': volume['display_name'],
- 'vol': volume['name'],
- 'new': new_vol_name})
-
- def _get_existing_volume_ref_name(self, existing_ref):
- """Returns the volume name of an existing reference.
-
- Checks if an existing volume reference has a source-name element.
- If source-name is not present an error will be thrown.
- """
- if 'source-name' not in existing_ref:
- reason = _("Reference must contain source-name.")
- raise exception.ManageExistingInvalidReference(
- existing_ref=existing_ref,
- reason=reason)
-
- return existing_ref['source-name']
-
- def _check_api_version(self):
- """Checks that the API version is correct."""
- if (self.api_version < MIN_API_VERSION):
- ex_msg = (_('Invalid HPLeftHand API version found: %(found)s. '
- 'Version %(minimum)s or greater required for '
- 'manage/unmanage support.')
- % {'found': self.api_version,
- 'minimum': MIN_API_VERSION})
- LOG.error(ex_msg)
- raise exception.InvalidInput(reason=ex_msg)
-
- def _get_volume_type(self, type_id):
- ctxt = context.get_admin_context()
- return volume_types.get_volume_type(ctxt, type_id)