Merge "3PAR with pool-aware-cinder-scheduler"

This commit is contained in:
Jenkins 2014-10-14 20:52:19 +00:00 committed by Gerrit Code Review
commit 0035c961d5
5 changed files with 402 additions and 122 deletions

View File

@ -31,6 +31,7 @@ from cinder.volume.drivers.san.hp import hp_3par_common as hpcommon
from cinder.volume.drivers.san.hp import hp_3par_fc as hpfcdriver
from cinder.volume.drivers.san.hp import hp_3par_iscsi as hpdriver
from cinder.volume import qos_specs
from cinder.volume import utils as volume_utils
from cinder.volume import volume_types
hpexceptions = hp3parclient.hpexceptions
@ -40,6 +41,8 @@ LOG = logging.getLogger(__name__)
CONF = cfg.CONF
HP3PAR_CPG = 'OpenStackCPG'
HP3PAR_CPG2 = 'fakepool'
HP3PAR_CPG_QOS = 'qospool'
HP3PAR_CPG_SNAP = 'OpenStackCPGSnap'
HP3PAR_USER_NAME = 'testUser'
HP3PAR_USER_PASS = 'testPassword'
@ -109,6 +112,14 @@ class HP3PARBaseDriver(object):
'volume_type': None,
'volume_type_id': None}
volume_pool = {'name': VOLUME_NAME,
'id': VOLUME_ID,
'display_name': 'Foo Volume',
'size': 2,
'host': volume_utils.append_host(FAKE_HOST, HP3PAR_CPG2),
'volume_type': None,
'volume_type_id': None}
volume_qos = {'name': VOLUME_NAME,
'id': VOLUME_ID,
'display_name': 'Foo Volume',
@ -140,7 +151,8 @@ class HP3PARBaseDriver(object):
volume_type = {'name': 'gold',
'deleted': False,
'updated_at': None,
'extra_specs': {'qos:maxIOPS': '1000',
'extra_specs': {'cpg': HP3PAR_CPG2,
'qos:maxIOPS': '1000',
'qos:maxBWS': '50',
'qos:minIOPS': '100',
'qos:minBWS': '25',
@ -259,7 +271,7 @@ class HP3PARBaseDriver(object):
'name': 'blue',
'id': RETYPE_VOLUME_TYPE_ID,
'extra_specs': {
'cpg': HP3PAR_CPG,
'cpg': HP3PAR_CPG_QOS,
'snap_cpg': HP3PAR_CPG_SNAP,
'vvs': RETYPE_VVS_NAME,
'qos': RETYPE_QOS_SPECS,
@ -353,7 +365,7 @@ class HP3PARBaseDriver(object):
configuration.hp3par_username = HP3PAR_USER_NAME
configuration.hp3par_password = HP3PAR_USER_PASS
configuration.hp3par_api_url = 'https://1.1.1.1/api/v1'
configuration.hp3par_cpg = HP3PAR_CPG
configuration.hp3par_cpg = [HP3PAR_CPG, HP3PAR_CPG2]
configuration.hp3par_cpg_snap = HP3PAR_CPG_SNAP
configuration.iscsi_ip_address = '1.1.1.2'
configuration.iscsi_port = '1234'
@ -411,6 +423,7 @@ class HP3PARBaseDriver(object):
conn_timeout=HP3PAR_SAN_SSH_CON_TIMEOUT),
mock.call.login(HP3PAR_USER_NAME, HP3PAR_USER_PASS),
mock.call.getCPG(HP3PAR_CPG),
mock.call.getCPG(HP3PAR_CPG2),
mock.call.logout()]
mock_client.assert_has_calls(expected)
@ -441,6 +454,7 @@ class HP3PARBaseDriver(object):
conn_timeout=HP3PAR_SAN_SSH_CON_TIMEOUT),
mock.call.login(HP3PAR_USER_NAME, HP3PAR_USER_PASS),
mock.call.getCPG(HP3PAR_CPG),
mock.call.getCPG(HP3PAR_CPG2),
mock.call.logout()]
mock_client.assert_has_calls(expected)
@ -471,6 +485,7 @@ class HP3PARBaseDriver(object):
conn_timeout=HP3PAR_SAN_SSH_CON_TIMEOUT),
mock.call.login(HP3PAR_USER_NAME, HP3PAR_USER_PASS),
mock.call.getCPG(HP3PAR_CPG),
mock.call.getCPG(HP3PAR_CPG2),
mock.call.logout()]
mock_client.assert_has_calls(expected)
@ -520,6 +535,30 @@ class HP3PARBaseDriver(object):
mock_client.assert_has_calls(expected)
def test_create_volume_in_pool(self):
# setup_mock_client drive with default configuration
# and return the mock HTTP 3PAR client
mock_client = self.setup_driver()
return_model = self.driver.create_volume(self.volume_pool)
comment = (
'{"display_name": "Foo Volume", "type": "OpenStack",'
' "name": "volume-d03338a9-9115-48a3-8dfc-35cdfcdc15a7",'
' "volume_id": "d03338a9-9115-48a3-8dfc-35cdfcdc15a7"}')
expected = [
mock.call.login(HP3PAR_USER_NAME, HP3PAR_USER_PASS),
mock.call.createVolume(
self.VOLUME_3PAR_NAME,
HP3PAR_CPG2,
1907, {
'comment': comment,
'tpvv': True,
'snapCPG': HP3PAR_CPG_SNAP}),
mock.call.logout()]
mock_client.assert_has_calls(expected)
self.assertEqual(return_model, None)
@mock.patch.object(volume_types, 'get_volume_type')
def test_create_volume_qos(self, _mock_volume_types):
# setup_mock_client drive with default configuration
@ -529,14 +568,14 @@ class HP3PARBaseDriver(object):
_mock_volume_types.return_value = {
'name': 'gold',
'extra_specs': {
'cpg': HP3PAR_CPG,
'cpg': HP3PAR_CPG_QOS,
'snap_cpg': HP3PAR_CPG_SNAP,
'vvs_name': self.VVS_NAME,
'qos': self.QOS,
'tpvv': True,
'volume_type': self.volume_type}}
self.driver.create_volume(self.volume_qos)
return_model = self.driver.create_volume(self.volume_qos)
comment = (
'{"volume_type_name": "gold", "display_name": "Foo Volume"'
', "name": "volume-d03338a9-9115-48a3-8dfc-35cdfcdc15a7'
@ -545,9 +584,10 @@ class HP3PARBaseDriver(object):
expected = [
mock.call.login(HP3PAR_USER_NAME, HP3PAR_USER_PASS),
mock.call.getCPG(HP3PAR_CPG_QOS),
mock.call.createVolume(
self.VOLUME_3PAR_NAME,
HP3PAR_CPG,
HP3PAR_CPG_QOS,
1907, {
'comment': comment,
'tpvv': True,
@ -555,6 +595,9 @@ class HP3PARBaseDriver(object):
mock.call.logout()]
mock_client.assert_has_calls(expected)
self.assertEqual(return_model,
{'host': volume_utils.append_host(self.FAKE_HOST,
HP3PAR_CPG_QOS)})
@mock.patch.object(volume_types, 'get_volume_type')
def test_retype_not_3par(self, _mock_volume_types):
@ -803,7 +846,9 @@ class HP3PARBaseDriver(object):
volume = {'id': HP3PARBaseDriver.CLONE_ID}
self.driver.retype(self.ctxt, volume, type_ref, None, self.RETYPE_HOST)
retyped = self.driver.retype(
self.ctxt, volume, type_ref, None, self.RETYPE_HOST)
self.assertTrue(retyped)
expected = [
mock.call.modifyVolume('osv-0DM4qZEVSKON-AAAAAAAAA',
@ -876,18 +921,51 @@ class HP3PARBaseDriver(object):
'id': HP3PARBaseDriver.CLONE_ID,
'display_name': 'Foo Volume',
'size': 2,
'host': HP3PARBaseDriver.FAKE_HOST,
'host': volume_utils.append_host(self.FAKE_HOST,
HP3PAR_CPG2),
'source_volid': HP3PARBaseDriver.VOLUME_ID}
src_vref = {}
model_update = self.driver.create_cloned_volume(volume, src_vref)
self.assertIsNotNone(model_update)
self.assertIsNone(model_update)
expected = [
mock.call.login(HP3PAR_USER_NAME, HP3PAR_USER_PASS),
mock.call.copyVolume(
self.VOLUME_3PAR_NAME,
'osv-0DM4qZEVSKON-AAAAAAAAA',
HP3PAR_CPG,
HP3PAR_CPG2,
{'snapCPG': 'OpenStackCPGSnap', 'tpvv': True,
'online': True}),
mock.call.logout()]
mock_client.assert_has_calls(expected)
@mock.patch.object(volume_types, 'get_volume_type')
def test_create_cloned_qos_volume(self, _mock_volume_types):
_mock_volume_types.return_value = self.RETYPE_VOLUME_TYPE_2
mock_client = self.setup_driver()
mock_client.copyVolume.return_value = {'taskid': 1}
src_vref = {}
volume = self.volume_qos.copy()
host = "TEST_HOST"
pool = "TEST_POOL"
volume_host = volume_utils.append_host(host, pool)
expected_cpg = self.RETYPE_VOLUME_TYPE_2['extra_specs']['cpg']
expected_volume_host = volume_utils.append_host(host, expected_cpg)
volume['id'] = HP3PARBaseDriver.CLONE_ID
volume['host'] = volume_host
volume['source_volid'] = HP3PARBaseDriver.VOLUME_ID
model_update = self.driver.create_cloned_volume(volume, src_vref)
self.assertEqual(model_update, {'host': expected_volume_host})
expected = [
mock.call.login(HP3PAR_USER_NAME, HP3PAR_USER_PASS),
mock.call.getCPG(expected_cpg),
mock.call.copyVolume(
self.VOLUME_3PAR_NAME,
'osv-0DM4qZEVSKON-AAAAAAAAA',
expected_cpg,
{'snapCPG': 'OpenStackCPGSnap', 'tpvv': True,
'online': True}),
mock.call.logout()]
@ -1146,7 +1224,9 @@ class HP3PARBaseDriver(object):
# setup_mock_client drive with default configuration
# and return the mock HTTP 3PAR client
mock_client = self.setup_driver()
self.driver.create_volume_from_snapshot(self.volume, self.snapshot)
model_update = self.driver.create_volume_from_snapshot(self.volume,
self.snapshot)
self.assertIsNone(model_update)
comment = (
'{"snapshot_id": "2f823bdc-e36e-4dc8-bd15-de1c7a28ff31",'
@ -1185,7 +1265,11 @@ class HP3PARBaseDriver(object):
volume = self.volume.copy()
volume['size'] = self.volume['size'] + 10
self.driver.create_volume_from_snapshot(volume, self.snapshot)
model_update = self.driver.create_volume_from_snapshot(volume,
self.snapshot)
self.assertEqual(model_update,
{'host': volume_utils.append_host(self.FAKE_HOST,
HP3PAR_CPG)})
comment = (
'{"snapshot_id": "2f823bdc-e36e-4dc8-bd15-de1c7a28ff31",'
@ -1204,7 +1288,68 @@ class HP3PARBaseDriver(object):
{
'comment': comment,
'readOnly': False}),
mock.call.copyVolume(osv_matcher, omv_matcher, mock.ANY, mock.ANY),
mock.call.copyVolume(
osv_matcher, omv_matcher, HP3PAR_CPG, mock.ANY),
mock.call.getTask(mock.ANY),
mock.call.getVolume(osv_matcher),
mock.call.deleteVolume(osv_matcher),
mock.call.modifyVolume(omv_matcher, {'newName': osv_matcher}),
mock.call.growVolume(osv_matcher, 10 * 1024),
mock.call.logout()]
mock_client.assert_has_calls(expected)
@mock.patch.object(volume_types, 'get_volume_type')
def test_create_volume_from_snapshot_and_extend_with_qos(
self, _mock_volume_types):
# setup_mock_client drive with default configuration
# and return the mock HTTP 3PAR client
conf = {
'getTask.return_value': {
'status': 1},
'copyVolume.return_value': {'taskid': 1},
'getVolume.return_value': {}
}
mock_client = self.setup_driver(mock_conf=conf)
_mock_volume_types.return_value = {
'name': 'gold',
'extra_specs': {
'cpg': HP3PAR_CPG_QOS,
'snap_cpg': HP3PAR_CPG_SNAP,
'vvs_name': self.VVS_NAME,
'qos': self.QOS,
'tpvv': True,
'volume_type': self.volume_type}}
volume = self.volume_qos.copy()
volume['size'] = self.volume['size'] + 10
model_update = self.driver.create_volume_from_snapshot(volume,
self.snapshot)
self.assertEqual(model_update,
{'host': volume_utils.append_host(self.FAKE_HOST,
HP3PAR_CPG_QOS)})
comment = (
'{"snapshot_id": "2f823bdc-e36e-4dc8-bd15-de1c7a28ff31",'
' "display_name": "Foo Volume",'
' "volume_id": "d03338a9-9115-48a3-8dfc-35cdfcdc15a7"}')
volume_name_3par = self.driver.common._encode_name(volume['id'])
osv_matcher = 'osv-' + volume_name_3par
omv_matcher = 'omv-' + volume_name_3par
expected = [
mock.call.login(HP3PAR_USER_NAME, HP3PAR_USER_PASS),
mock.call.createSnapshot(
self.VOLUME_3PAR_NAME,
'oss-L4I73ONuTci9Fd4ceij-MQ',
{
'comment': comment,
'readOnly': False}),
mock.call.getCPG(HP3PAR_CPG_QOS),
mock.call.copyVolume(
osv_matcher, omv_matcher, HP3PAR_CPG_QOS, mock.ANY),
mock.call.getTask(mock.ANY),
mock.call.getVolume(osv_matcher),
mock.call.deleteVolume(osv_matcher),
@ -1537,6 +1682,7 @@ class HP3PARBaseDriver(object):
"type": "OpenStack"}
volume = {'display_name': None,
'host': 'my-stack1@3parxxx#CPGNOTUSED',
'volume_type': 'gold',
'volume_type_id': 'acfa9fa4-54a0-4340-a3d8-bfcf19aea65e',
'id': '007dbfce-7579-40bc-8f90-a20b3902283e'}
@ -1552,7 +1698,8 @@ class HP3PARBaseDriver(object):
obj = self.driver.manage_existing(volume, existing_ref)
expected_obj = {'display_name': 'Foo Volume'}
expected_obj = {'display_name': 'Foo Volume',
'host': 'my-stack1@3parxxx#fakepool'}
expected_manage = [
mock.call.login(HP3PAR_USER_NAME, HP3PAR_USER_PASS),
@ -1593,10 +1740,11 @@ class HP3PARBaseDriver(object):
'bwMinGoalKB': 25600, 'priority': 1, 'latencyGoal': 25,
'bwMaxLimitKB': 51200}),
mock.call.addVolumeToVolumeSet(vvs_matcher, osv_matcher),
mock.call.modifyVolume(osv_matcher,
{'action': 6, 'userCPG': 'OpenStackCPG',
'conversionOperation': 1,
'tuneOperation': 1}),
mock.call.modifyVolume(
osv_matcher,
{'action': 6,
'userCPG': self.volume_type['extra_specs']['cpg'],
'conversionOperation': 1, 'tuneOperation': 1}),
mock.call.getTask(1),
mock.call.logout()
]
@ -1624,6 +1772,7 @@ class HP3PARBaseDriver(object):
"type": "OpenStack"}
volume = {'display_name': 'Test Volume',
'host': 'my-stack1@3parxxx#CPGNOTUSED',
'volume_type': 'gold',
'volume_type_id': 'acfa9fa4-54a0-4340-a3d8-bfcf19aea65e',
'id': id}
@ -1636,7 +1785,8 @@ class HP3PARBaseDriver(object):
obj = self.driver.manage_existing(volume, existing_ref)
expected_obj = {'display_name': 'Test Volume'}
expected_obj = {'display_name': 'Test Volume',
'host': 'my-stack1@3parxxx#qospool'}
expected_manage = [
mock.call.login(HP3PAR_USER_NAME, HP3PAR_USER_PASS),
mock.call.getVolume(existing_ref['source-name']),
@ -1661,7 +1811,8 @@ class HP3PARBaseDriver(object):
mock.call.deleteVolumeSet(vvs_matcher),
mock.call.addVolumeToVolumeSet(vvs, osv_matcher),
mock.call.modifyVolume(osv_matcher,
{'action': 6, 'userCPG': 'OpenStackCPG',
{'action': 6, 'userCPG':
test_volume_type['extra_specs']['cpg'],
'conversionOperation': 1,
'tuneOperation': 1}),
mock.call.getTask(1),
@ -1976,6 +2127,7 @@ class TestHP3PARFCDriver(HP3PARBaseDriver, test.TestCase):
conn_timeout=HP3PAR_SAN_SSH_CON_TIMEOUT),
mock.call.login(HP3PAR_USER_NAME, HP3PAR_USER_PASS),
mock.call.getCPG(HP3PAR_CPG),
mock.call.getCPG(HP3PAR_CPG2),
mock.call.logout()]
mock_client.assert_has_calls(expected)
mock_client.reset_mock()
@ -2264,30 +2416,33 @@ class TestHP3PARFCDriver(HP3PARBaseDriver, test.TestCase):
# and return the mock HTTP 3PAR client
mock_client = self.setup_driver()
mock_client.getCPG.return_value = self.cpgs[0]
totalCapacityMiB = 8000
freeCapacityMiB = 4000
mock_client.getStorageSystemInfo.return_value = {
'serialNumber': '1234',
'totalCapacityMiB': totalCapacityMiB,
'freeCapacityMiB': freeCapacityMiB
'freeCapacityMiB': 1024.0 * 2,
'totalCapacityMiB': 1024.0 * 123
}
stats = self.driver.get_volume_stats(True)
const = 0.0009765625
self.assertEqual(stats['storage_protocol'], 'FC')
self.assertEqual(stats['total_capacity_gb'], totalCapacityMiB * const)
self.assertEqual(stats['free_capacity_gb'], freeCapacityMiB * const)
self.assertEqual(stats['total_capacity_gb'], 0)
self.assertEqual(stats['free_capacity_gb'], 0)
self.assertEqual(stats['pools'][0]['total_capacity_gb'], 123.0)
self.assertEqual(stats['pools'][0]['free_capacity_gb'], 2.0)
expected = [
mock.call.login(HP3PAR_USER_NAME, HP3PAR_USER_PASS),
mock.call.getStorageSystemInfo(),
mock.call.getCPG(HP3PAR_CPG),
mock.call.getCPG(HP3PAR_CPG2),
mock.call.logout()]
mock_client.assert_has_calls(expected)
stats = self.driver.get_volume_stats(True)
self.assertEqual(stats['storage_protocol'], 'FC')
self.assertEqual(stats['total_capacity_gb'], totalCapacityMiB * const)
self.assertEqual(stats['free_capacity_gb'], freeCapacityMiB * const)
self.assertEqual(stats['total_capacity_gb'], 0)
self.assertEqual(stats['free_capacity_gb'], 0)
self.assertEqual(stats['pools'][0]['total_capacity_gb'], 123.0)
self.assertEqual(stats['pools'][0]['free_capacity_gb'], 2.0)
cpg2 = self.cpgs[0].copy()
cpg2.update({'SDGrowth': {'limitMiB': 8192}})
@ -2296,10 +2451,14 @@ class TestHP3PARFCDriver(HP3PARBaseDriver, test.TestCase):
stats = self.driver.get_volume_stats(True)
self.assertEqual(stats['storage_protocol'], 'FC')
total_capacity_gb = 8192 * const
self.assertEqual(stats['total_capacity_gb'], total_capacity_gb)
self.assertEqual(stats['total_capacity_gb'], 0)
self.assertEqual(stats['pools'][0]['total_capacity_gb'],
total_capacity_gb)
free_capacity_gb = int(
(8192 - self.cpgs[0]['UsrUsage']['usedMiB']) * const)
self.assertEqual(stats['free_capacity_gb'], free_capacity_gb)
self.assertEqual(stats['free_capacity_gb'], 0)
self.assertEqual(stats['pools'][0]['free_capacity_gb'],
free_capacity_gb)
self.driver.common.client.deleteCPG(HP3PAR_CPG)
self.driver.common.client.createCPG(HP3PAR_CPG, {})
@ -2501,6 +2660,7 @@ class TestHP3PARISCSIDriver(HP3PARBaseDriver, test.TestCase):
conn_timeout=HP3PAR_SAN_SSH_CON_TIMEOUT),
mock.call.login(HP3PAR_USER_NAME, HP3PAR_USER_PASS),
mock.call.getCPG(HP3PAR_CPG),
mock.call.getCPG(HP3PAR_CPG2),
mock.call.logout(),
mock.call.login(HP3PAR_USER_NAME, HP3PAR_USER_PASS),
mock.call.getPorts(),
@ -2557,23 +2717,24 @@ class TestHP3PARISCSIDriver(HP3PARBaseDriver, test.TestCase):
# and return the mock HTTP 3PAR client
mock_client = self.setup_driver()
mock_client.getCPG.return_value = self.cpgs[0]
totalCapacityMiB = 8000
freeCapacityMiB = 4000
mock_client.getStorageSystemInfo.return_value = {
'serialNumber': '1234',
'totalCapacityMiB': totalCapacityMiB,
'freeCapacityMiB': freeCapacityMiB
'freeCapacityMiB': 1024.0 * 2,
'totalCapacityMiB': 1024.0 * 123
}
stats = self.driver.get_volume_stats(True)
const = 0.0009765625
self.assertEqual(stats['storage_protocol'], 'iSCSI')
self.assertEqual(stats['total_capacity_gb'], totalCapacityMiB * const)
self.assertEqual(stats['free_capacity_gb'], freeCapacityMiB * const)
self.assertEqual(stats['total_capacity_gb'], 0)
self.assertEqual(stats['free_capacity_gb'], 0)
self.assertEqual(stats['pools'][0]['total_capacity_gb'], 123.0)
self.assertEqual(stats['pools'][0]['free_capacity_gb'], 2.0)
expected = [
mock.call.login(HP3PAR_USER_NAME, HP3PAR_USER_PASS),
mock.call.getStorageSystemInfo(),
mock.call.getCPG(HP3PAR_CPG),
mock.call.getCPG(HP3PAR_CPG2),
mock.call.logout()]
mock_client.assert_has_calls(expected)
@ -2585,10 +2746,14 @@ class TestHP3PARISCSIDriver(HP3PARBaseDriver, test.TestCase):
stats = self.driver.get_volume_stats(True)
self.assertEqual(stats['storage_protocol'], 'iSCSI')
total_capacity_gb = 8192 * const
self.assertEqual(stats['total_capacity_gb'], total_capacity_gb)
self.assertEqual(stats['total_capacity_gb'], 0)
self.assertEqual(stats['pools'][0]['total_capacity_gb'],
total_capacity_gb)
free_capacity_gb = int(
(8192 - self.cpgs[0]['UsrUsage']['usedMiB']) * const)
self.assertEqual(stats['free_capacity_gb'], free_capacity_gb)
self.assertEqual(stats['free_capacity_gb'], 0)
self.assertEqual(stats['pools'][0]['free_capacity_gb'],
free_capacity_gb)
def test_create_host(self):
# setup_mock_client drive with default configuration
@ -3325,6 +3490,24 @@ class TestHP3PARISCSIDriver(HP3PARBaseDriver, test.TestCase):
mock_client.assert_has_calls(expected)
self.assertEqual(model, expected_model)
@mock.patch.object(volume_types, 'get_volume_type')
def test_get_volume_settings_default_pool(self, _mock_volume_types):
_mock_volume_types.return_value = {
'name': 'gold',
'id': 'gold-id',
'extra_specs': {}}
self.setup_driver()
volume = {'host': 'test-host@3pariscsi#pool_foo',
'id': 'd03338a9-9115-48a3-8dfc-35cdfcdc15a7'}
model = self.driver.common.get_volume_settings_from_type_id('gold-id',
volume)
self.assertEqual(model['cpg'], 'pool_foo')
def test_get_model_update(self):
self.setup_driver()
model_update = self.driver.common._get_model_update('xxx@yyy#zzz',
'CPG')
self.assertEqual(model_update, {'host': 'xxx@yyy#CPG'})
VLUNS5_RET = ({'members':
[{'portPos': {'node': 0, 'slot': 8, 'cardPort': 2},

View File

@ -59,6 +59,7 @@ from cinder.openstack.common import log as logging
from cinder.openstack.common import loopingcall
from cinder.openstack.common import units
from cinder.volume import qos_specs
from cinder.volume import utils as volume_utils
from cinder.volume import volume_types
import taskflow.engines
@ -81,13 +82,13 @@ hp3par_opts = [
default='',
help="3PAR Super user password",
secret=True),
cfg.StrOpt('hp3par_cpg',
default="OpenStack",
help="The CPG to use for volume creation"),
cfg.ListOpt('hp3par_cpg',
default=["OpenStack"],
help="List of the CPG(s) to use for volume creation"),
cfg.StrOpt('hp3par_cpg_snap',
default="",
help="The CPG to use for Snapshots for volumes. "
"If empty hp3par_cpg will be used"),
"If empty the userCPG will be used."),
cfg.StrOpt('hp3par_snapshot_retention',
default="",
help="The time in hours to retain a snapshot. "
@ -151,10 +152,11 @@ class HP3PARCommon(object):
2.0.21 - Remove bogus invalid snapCPG=None exception
2.0.22 - HP 3PAR drivers should not claim to have 'infinite' space
2.0.23 - Increase the hostname size from 23 to 31 Bug #1371242
2.0.24 - Add pools (hp3par_cpg now accepts a list of CPGs)
"""
VERSION = "2.0.23"
VERSION = "2.0.24"
stats = {}
@ -270,8 +272,10 @@ class HP3PARCommon(object):
self.client_login()
try:
# make sure the default CPG exists
self.validate_cpg(self.config.hp3par_cpg)
cpg_names = self.config.hp3par_cpg
for cpg_name in cpg_names:
self.validate_cpg(cpg_name)
finally:
self.client_logout()
@ -365,12 +369,15 @@ class HP3PARCommon(object):
LOG.info(_("Virtual volume '%(ref)s' renamed to '%(new)s'.") %
{'ref': existing_ref['source-name'], 'new': new_vol_name})
retyped = False
model_update = None
if volume_type:
LOG.info(_("Virtual volume %(disp)s '%(new)s' is being retyped.") %
{'disp': display_name, 'new': new_vol_name})
try:
self._retype_from_no_type(volume, volume_type)
retyped, model_update = self._retype_from_no_type(volume,
volume_type)
LOG.info(_("Virtual volume %(disp)s successfully retyped to "
"%(new_type)s.") %
{'disp': display_name,
@ -386,11 +393,16 @@ class HP3PARCommon(object):
{'newName': existing_ref['source-name'],
'comment': old_comment_str})
updates = {'display_name': display_name}
if retyped and model_update:
updates.update(model_update)
LOG.info(_("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.
return {'display_name': display_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.
@ -439,30 +451,33 @@ class HP3PARCommon(object):
def _extend_volume(self, volume, volume_name, growth_size_mib,
_convert_to_base=False):
model_update = None
try:
if _convert_to_base:
LOG.debug("Converting to base volume prior to growing.")
self._convert_to_base_volume(volume)
model_update = self._convert_to_base_volume(volume)
self.client.growVolume(volume_name, growth_size_mib)
except Exception as ex:
with excutils.save_and_reraise_exception() as ex_ctxt:
if (not _convert_to_base and
isinstance(ex, hpexceptions.HTTPForbidden) and
ex.get_code() == 150):
# Error code 150 means 'invalid operation: Cannot grow
# this type of volume'.
# Suppress raising this exception because we can
# resolve it by converting it into a base volume.
# Afterwards, extending the volume should succeed, or
# fail with a different exception/error code.
ex_ctxt.reraise = False
self._extend_volume(volume, volume_name,
growth_size_mib,
_convert_to_base=True)
# Error code 150 means 'invalid operation: Cannot grow
# this type of volume'.
# Suppress raising this exception because we can
# resolve it by converting it into a base volume.
# Afterwards, extending the volume should succeed, or
# fail with a different exception/error code.
ex_ctxt.reraise = False
model_update = self._extend_volume(
volume, volume_name,
growth_size_mib,
_convert_to_base=True)
else:
LOG.error(_("Error extending volume: %(vol)s. "
"Exception: %(ex)s") %
{'vol': volume_name, 'ex': ex})
return model_update
def _get_3par_vol_name(self, volume_id):
"""Get converted 3PAR volume name.
@ -616,40 +631,47 @@ class HP3PARCommon(object):
# storage_protocol and volume_backend_name are
# set in the child classes
stats = {'driver_version': '1.0',
'free_capacity_gb': 'unknown',
'reserved_percentage': 0,
'storage_protocol': None,
'total_capacity_gb': 'unknown',
'QoS_support': True,
'vendor_name': 'Hewlett-Packard',
'volume_backend_name': None}
pools = []
info = self.client.getStorageSystemInfo()
try:
cpg = self.client.getCPG(self.config.hp3par_cpg)
if 'limitMiB' not in cpg['SDGrowth']:
# System capacity is best we can do for now.
total_capacity = info['totalCapacityMiB'] * const
free_capacity = info['freeCapacityMiB'] * const
else:
total_capacity = int(cpg['SDGrowth']['limitMiB'] * const)
free_capacity = int((cpg['SDGrowth']['limitMiB'] -
cpg['UsrUsage']['usedMiB']) * const)
for cpg_name in self.config.hp3par_cpg:
try:
cpg = self.client.getCPG(cpg_name)
if 'limitMiB' not in cpg['SDGrowth']:
# System capacity is best we can do for now.
total_capacity = info['totalCapacityMiB'] * const
free_capacity = info['freeCapacityMiB'] * const
else:
total_capacity = int(cpg['SDGrowth']['limitMiB'] * const)
free_capacity = int((cpg['SDGrowth']['limitMiB'] -
cpg['UsrUsage']['usedMiB']) * const)
stats['total_capacity_gb'] = total_capacity
stats['free_capacity_gb'] = free_capacity
except hpexceptions.HTTPNotFound:
err = (_("CPG (%s) doesn't exist on array")
% self.config.hp3par_cpg)
LOG.error(err)
raise exception.InvalidInput(reason=err)
except hpexceptions.HTTPNotFound:
err = (_("CPG (%s) doesn't exist on array")
% cpg_name)
LOG.error(err)
raise exception.InvalidInput(reason=err)
stats['location_info'] = ('HP3PARDriver:%(sys_id)s:%(dest_cpg)s' %
{'sys_id': info['serialNumber'],
'dest_cpg': self.config.safe_get(
'hp3par_cpg')})
self.stats = stats
pool = {'pool_name': cpg_name,
'total_capacity_gb': total_capacity,
'free_capacity_gb': free_capacity,
'QoS_support': True,
'reserved_percentage': 0,
'location_info': ('HP3PARDriver:%(sys_id)s:%(dest_cpg)s' %
{'sys_id': info['serialNumber'],
'dest_cpg': cpg_name})
}
pools.append(pool)
self.stats = {'driver_version': '1.0',
'storage_protocol': None,
'vendor_name': 'Hewlett-Packard',
'volume_backend_name': None,
# Use zero capacities here so we always use a pool.
'total_capacity_gb': 0,
'free_capacity_gb': 0,
'reserved_percentage': 0,
'pools': pools}
def _get_vlun(self, volume_name, hostname, lun_id=None):
"""find a VLUN on a 3PAR host."""
@ -921,11 +943,13 @@ class HP3PARCommon(object):
qos = self._get_qos_by_volume_type(volume_type)
return hp3par_keys, qos, volume_type, vvs_name
def get_volume_settings_from_type_id(self, type_id):
def get_volume_settings_from_type_id(self, type_id, volume):
"""Get 3PAR volume settings given a type_id.
Combines type info and config settings to return a dictionary
describing the 3PAR volume settings. Does some validation (CPG).
Uses volume['host'] to determine default cpg (when not specified in
volume type specs).
:param type_id:
:return: dict
@ -933,9 +957,22 @@ class HP3PARCommon(object):
hp3par_keys, qos, volume_type, vvs_name = self.get_type_info(type_id)
cpg = self._get_key_value(hp3par_keys, 'cpg',
self.config.hp3par_cpg)
if cpg is not self.config.hp3par_cpg:
# Default to 1st configured CPG unless we can extract pool from host.
default_cpg = self.config.hp3par_cpg[0]
try:
pool = volume_utils.extract_host(volume['host'], 'pool')
if pool:
default_cpg = pool
LOG.debug("Default CPG from volume['host'] is (%s)" %
default_cpg)
else:
LOG.debug("Default CPG from volume['host'] not found")
except Exception as ex:
LOG.debug("Default CPG from volume['host'] not found due to (%s)" %
ex)
cpg = self._get_key_value(hp3par_keys, 'cpg', default_cpg)
if cpg not in self.config.hp3par_cpg:
# The cpg was specified in a volume type extra spec so it
# needs to be validated that it's in the correct domain.
self.validate_cpg(cpg)
@ -987,7 +1024,8 @@ class HP3PARCommon(object):
type_id = volume.get('volume_type_id', None)
volume_settings = self.get_volume_settings_from_type_id(type_id)
volume_settings = self.get_volume_settings_from_type_id(type_id,
volume)
# check for valid persona even if we don't use it until
# attach time, this will give the end user notice that the
@ -1060,6 +1098,8 @@ class HP3PARCommon(object):
LOG.error(ex)
raise exception.CinderException(ex)
return self._get_model_update(volume['host'], cpg)
def _copy_volume(self, src_name, dest_name, cpg, snap_cpg=None,
tpvv=True):
# Virtual volume sets are not supported with the -online option
@ -1088,6 +1128,32 @@ class HP3PARCommon(object):
return comment_dict[key]
return None
def _get_model_update(self, volume_host, cpg):
"""Get model_update dict to use when we select a pool.
The pools implementation uses a volume['host'] suffix of :poolname.
When the volume comes in with this selected pool, we sometimes use
a different pool (e.g. because the type says to use a different pool).
So in the several places that we do this, we need to return a model
update so that the volume will have the actual pool name in the host
suffix after the operation.
Given a volume_host, which should (might) have the pool suffix, and
given the CPG we actually chose to use, return a dict to use for a
model update iff an update is needed.
:param volume_host: The volume's host string.
:param cpg: The actual pool (cpg) used, for example from the type.
:return: dict Model update if we need to update volume host, else None
"""
model_update = None
host = volume_utils.extract_host(volume_host, 'backend')
host_and_pool = volume_utils.append_host(host, cpg)
if volume_host != host_and_pool:
# Since we selected a pool based on type, update the model.
model_update = {'host': host_and_pool}
return model_update
def create_cloned_volume(self, volume, src_vref):
try:
orig_name = self._get_3par_vol_name(volume['source_volid'])
@ -1097,10 +1163,13 @@ class HP3PARCommon(object):
# make the 3PAR copy the contents.
# can't delete the original until the copy is done.
self._copy_volume(orig_name, vol_name, cpg=type_info['cpg'],
cpg = type_info['cpg']
self._copy_volume(orig_name, vol_name, cpg=cpg,
snap_cpg=type_info['snap_cpg'],
tpvv=type_info['tpvv'])
return None
return self._get_model_update(volume['host'], cpg)
except hpexceptions.HTTPForbidden:
raise exception.NotAuthorized()
except hpexceptions.HTTPNotFound:
@ -1188,6 +1257,7 @@ class HP3PARCommon(object):
(pprint.pformat(volume['display_name']),
pprint.pformat(snapshot['display_name'])))
model_update = None
if volume['size'] < snapshot['volume_size']:
err = ("You cannot reduce size of the volume. It must "
"be greater than or equal to the snapshot.")
@ -1225,7 +1295,7 @@ class HP3PARCommon(object):
try:
LOG.debug('Converting to base volume type: %s.' %
volume['id'])
self._convert_to_base_volume(volume)
model_update = self._convert_to_base_volume(volume)
growth_size_mib = growth_size * units.Gi / units.Mi
LOG.debug('Growing volume: %(id)s by %(size)s GiB.' %
{'id': volume['id'], 'size': growth_size})
@ -1238,11 +1308,11 @@ class HP3PARCommon(object):
raise exception.CinderException(ex)
if qos or vvs_name is not None:
cpg = self._get_key_value(hp3par_keys, 'cpg',
self.config.hp3par_cpg)
cpg_names = self._get_key_value(hp3par_keys, 'cpg',
self.config.hp3par_cpg)
try:
self._add_volume_to_volume_set(volume, volume_name,
cpg, vvs_name, qos)
cpg_names[0], vvs_name, qos)
except Exception as ex:
# Delete the volume if unable to add it to the volume set
self.client.deleteVolume(volume_name)
@ -1257,6 +1327,7 @@ class HP3PARCommon(object):
except Exception as ex:
LOG.error(ex)
raise exception.CinderException(ex)
return model_update
def create_snapshot(self, snapshot):
LOG.debug("Create Snapshot\n%s" % pprint.pformat(snapshot))
@ -1489,6 +1560,8 @@ class HP3PARCommon(object):
LOG.error(ex)
raise exception.CinderException(ex)
return self._get_model_update(volume['host'], cpg)
def delete_snapshot(self, snapshot):
LOG.debug("Delete Snapshot id %s %s" % (snapshot['id'],
pprint.pformat(snapshot)))
@ -1733,7 +1806,7 @@ class HP3PARCommon(object):
new_type_name = new_type['name']
new_type_id = new_type['id']
new_volume_settings = self.get_volume_settings_from_type_id(
new_type_id)
new_type_id, volume)
new_cpg = new_volume_settings['cpg']
new_snap_cpg = new_volume_settings['snap_cpg']
new_tpvv = new_volume_settings['tpvv']
@ -1765,7 +1838,11 @@ class HP3PARCommon(object):
host, new_persona, old_cpg, new_cpg,
old_snap_cpg, new_snap_cpg, old_tpvv, new_tpvv,
old_vvs, new_vvs, old_qos, new_qos, old_comment)
return True
if host:
return True, self._get_model_update(host['host'], new_cpg)
else:
return True, self._get_model_update(volume['host'], new_cpg)
def _retype_from_no_type(self, volume, new_type):
"""Convert the volume to be of the new type. Starting from no type.
@ -1777,7 +1854,8 @@ class HP3PARCommon(object):
volume-type is not used here. This method uses None.
:param new_type: A dictionary describing the volume type to convert to
"""
none_type_settings = self.get_volume_settings_from_type_id(None)
none_type_settings = self.get_volume_settings_from_type_id(
None, volume)
return self._retype_from_old_to_new(volume, new_type,
none_type_settings, None)

View File

@ -34,6 +34,7 @@ try:
except ImportError:
hpexceptions = None
from cinder import exception
from cinder.i18n import _
from cinder.openstack.common import log as logging
from cinder import utils
@ -69,10 +70,11 @@ class HP3PARFCDriver(cinder.volume.driver.FibreChannelDriver):
2.0.7 - Only one FC port is used when a single FC path
is present. bug #1360001
2.0.8 - Fixing missing login/logout around attach/detach bug #1367429
2.0.9 - Add support for pools with model update
"""
VERSION = "2.0.8"
VERSION = "2.0.9"
def __init__(self, *args, **kwargs):
super(HP3PARFCDriver, self).__init__(*args, **kwargs)
@ -118,8 +120,7 @@ class HP3PARFCDriver(cinder.volume.driver.FibreChannelDriver):
def create_volume(self, volume):
self.common.client_login()
try:
metadata = self.common.create_volume(volume)
return {'metadata': metadata}
return self.common.create_volume(volume)
finally:
self.common.client_logout()
@ -127,8 +128,7 @@ class HP3PARFCDriver(cinder.volume.driver.FibreChannelDriver):
def create_cloned_volume(self, volume, src_vref):
self.common.client_login()
try:
new_vol = self.common.create_cloned_volume(volume, src_vref)
return {'metadata': new_vol}
return self.common.create_cloned_volume(volume, src_vref)
finally:
self.common.client_logout()
@ -148,9 +148,8 @@ class HP3PARFCDriver(cinder.volume.driver.FibreChannelDriver):
"""
self.common.client_login()
try:
metadata = self.common.create_volume_from_snapshot(volume,
snapshot)
return {'metadata': metadata}
return self.common.create_volume_from_snapshot(volume,
snapshot)
finally:
self.common.client_logout()
@ -466,3 +465,14 @@ class HP3PARFCDriver(cinder.volume.driver.FibreChannelDriver):
return self.common.migrate_volume(volume, host)
finally:
self.common.client_logout()
def get_pool(self, volume):
self.common.client_login()
try:
return self.common.get_cpg(volume)
except hpexceptions.HTTPNotFound:
reason = (_("Volume %s doesn't exist on array.") % volume)
LOG.error(reason)
raise exception.InvalidVolume(reason)
finally:
self.common.client_logout()

View File

@ -74,10 +74,11 @@ class HP3PARISCSIDriver(cinder.volume.driver.ISCSIDriver):
2.0.5 - Added CHAP support, requires 3.1.3 MU1 firmware
and hp3parclient 3.1.0.
2.0.6 - Fixing missing login/logout around attach/detach bug #1367429
2.0.7 - Add support for pools with model update
"""
VERSION = "2.0.6"
VERSION = "2.0.7"
def __init__(self, *args, **kwargs):
super(HP3PARISCSIDriver, self).__init__(*args, **kwargs)
@ -189,8 +190,7 @@ class HP3PARISCSIDriver(cinder.volume.driver.ISCSIDriver):
def create_volume(self, volume):
self.common.client_login()
try:
metadata = self.common.create_volume(volume)
return {'metadata': metadata}
return self.common.create_volume(volume)
finally:
self.common.client_logout()
@ -199,8 +199,7 @@ class HP3PARISCSIDriver(cinder.volume.driver.ISCSIDriver):
"""Clone an existing volume."""
self.common.client_login()
try:
new_vol = self.common.create_cloned_volume(volume, src_vref)
return {'metadata': new_vol}
return self.common.create_cloned_volume(volume, src_vref)
finally:
self.common.client_logout()
@ -220,9 +219,8 @@ class HP3PARISCSIDriver(cinder.volume.driver.ISCSIDriver):
"""
self.common.client_login()
try:
metadata = self.common.create_volume_from_snapshot(volume,
snapshot)
return {'metadata': metadata}
return self.common.create_volume_from_snapshot(volume,
snapshot)
finally:
self.common.client_logout()
@ -674,3 +672,14 @@ class HP3PARISCSIDriver(cinder.volume.driver.ISCSIDriver):
return self.common.migrate_volume(volume, host)
finally:
self.common.client_logout()
def get_pool(self, volume):
self.common.client_login()
try:
return self.common.get_cpg(volume)
except hpexceptions.HTTPNotFound:
reason = (_("Volume %s doesn't exist on array.") % volume)
LOG.error(reason)
raise exception.InvalidVolume(reason)
finally:
self.common.client_logout()

View File

@ -1920,11 +1920,11 @@
# 3PAR Super user password (string value)
#hp3par_password=
# The CPG to use for volume creation (string value)
# List of the CPG(s) to use for volume creation (list value)
#hp3par_cpg=OpenStack
# The CPG to use for Snapshots for volumes. If empty
# hp3par_cpg will be used (string value)
# The CPG to use for Snapshots for volumes. If empty the
# userCPG will be used. (string value)
#hp3par_cpg_snap=
# The time in hours to retain a snapshot. You can't delete it