Skip to content

Commit

Permalink
Merge "3PAR Driver modifications to support QOS"
Browse files Browse the repository at this point in the history
  • Loading branch information
Jenkins authored and openstack-gerrit committed Jul 22, 2013
2 parents 6e95e1a + a048cd6 commit 767a018
Show file tree
Hide file tree
Showing 4 changed files with 204 additions and 24 deletions.
91 changes: 75 additions & 16 deletions cinder/tests/test_hp3par.py
Expand Up @@ -305,6 +305,7 @@ class HP3PARBaseDriver():
VOLUME_ID_SNAP = '761fc5e5-5191-4ec7-aeba-33e36de44156'
FAKE_DESC = 'test description name'
FAKE_FC_PORTS = ['0987654321234', '123456789000987']
QOS = "{'qos:maxIOPS': 1000, 'qos:maxBWS': 50}"
FAKE_ISCSI_PORTS = {'1.1.1.2': {'nsp': '8:1:1',
'iqn': ('iqn.2000-05.com.3pardata:'
'21810002ac00383d'),
Expand All @@ -318,6 +319,14 @@ class HP3PARBaseDriver():
'volume_type': None,
'volume_type_id': None}

volume_qos = {'name': VOLUME_NAME,
'id': VOLUME_ID,
'display_name': 'Foo Volume',
'size': 2,
'host': FAKE_HOST,
'volume_type': None,
'volume_type_id': 'gold'}

snapshot = {'name': SNAPSHOT_NAME,
'id': SNAPSHOT_ID,
'user_id': USER_ID,
Expand All @@ -336,6 +345,14 @@ class HP3PARBaseDriver():
'wwnns': ["223456789012345", "223456789054321"],
'host': 'fakehost'}

volume_type = {'name': 'gold',
'deleted': False,
'updated_at': None,
'extra_specs': {'qos:maxBWS': '50',
'qos:maxIOPS': '1000'},
'deleted_at': None,
'id': 'gold'}

def setup_configuration(self):
configuration = mox.MockObject(conf.Configuration)
configuration.hp3par_debug = False
Expand Down Expand Up @@ -401,12 +418,47 @@ def fake_create_3par_vlun(self, volume, hostname):
def fake_get_ports(self):
return {'FC': self.FAKE_FC_PORTS, 'iSCSI': self.FAKE_ISCSI_PORTS}

def fake_get_volume_type(self, type_id):
return self.volume_type

def fake_get_qos_by_volume_type(self, volume_type):
return self.QOS

def fake_add_volume_to_volume_set(self, volume, volume_name,
cpg, vvs_name, qos):
return volume

def fake_copy_volume(self, src_name, dest_name):
pass

def fake_get_volume_state(self, vol_name):
return "normal"

def test_create_volume(self):
self.flags(lock_path=self.tempdir)
model_update = self.driver.create_volume(self.volume)
metadata = model_update['metadata']
self.assertFalse(metadata['3ParName'] is None)
self.assertEqual(metadata['CPG'], HP3PAR_CPG)
self.assertEqual(metadata['snapCPG'], HP3PAR_CPG_SNAP)

def test_create_volume_qos(self):
self.flags(lock_path=self.tempdir)
self.stubs.Set(hpdriver.hpcommon.HP3PARCommon, "_get_volume_type",
self.fake_get_volume_type)
self.stubs.Set(hpdriver.hpcommon.HP3PARCommon,
"_get_qos_by_volume_type",
self.fake_get_qos_by_volume_type)
self.stubs.Set(hpdriver.hpcommon.HP3PARCommon,
"_add_volume_to_volume_set",
self.fake_add_volume_to_volume_set)
model_update = self.driver.create_volume(self.volume_qos)
metadata = model_update['metadata']
self.assertFalse(metadata['3ParName'] is None)
self.assertEqual(metadata['CPG'], HP3PAR_CPG)
self.assertEqual(metadata['snapCPG'], HP3PAR_CPG_SNAP)
self.assertEqual(metadata['qos'], True)

def test_delete_volume(self):
self.flags(lock_path=self.tempdir)
self.driver.delete_volume(self.volume)
Expand Down Expand Up @@ -449,6 +501,29 @@ def test_create_volume_from_snapshot(self):
self.driver.create_volume_from_snapshot,
volume, self.snapshot)

def test_create_volume_from_snapshot_qos(self):
self.flags(lock_path=self.tempdir)
self.stubs.Set(hpdriver.hpcommon.HP3PARCommon, "_get_volume_type",
self.fake_get_volume_type)
self.stubs.Set(hpdriver.hpcommon.HP3PARCommon,
"_get_qos_by_volume_type",
self.fake_get_qos_by_volume_type)
self.stubs.Set(hpdriver.hpcommon.HP3PARCommon,
"_add_volume_to_volume_set",
self.fake_add_volume_to_volume_set)
model_update = self.driver.create_volume_from_snapshot(self.volume_qos,
self.snapshot)
metadata = model_update['metadata']
self.assertEqual(metadata['qos'], True)
snap_vol = self.driver.common.client.getVolume(self.VOLUME_3PAR_NAME)
self.assertEqual(snap_vol['name'], self.VOLUME_3PAR_NAME)

volume = self.volume.copy()
volume['size'] = 1
self.assertRaises(exception.InvalidInput,
self.driver.create_volume_from_snapshot,
volume, self.snapshot)

def test_terminate_connection(self):
self.flags(lock_path=self.tempdir)
#setup the connections
Expand Down Expand Up @@ -521,14 +596,6 @@ def fake_create_3par_fibrechan_host(self, hostname, wwn,
'driver_volume_type': 'fibre_channel'}
return hostname

def test_create_volume(self):
self.flags(lock_path=self.tempdir)
model_update = self.driver.create_volume(self.volume)
metadata = model_update['metadata']
self.assertFalse(metadata['3ParName'] is None)
self.assertEqual(metadata['CPG'], HP3PAR_CPG)
self.assertEqual(metadata['snapCPG'], HP3PAR_CPG_SNAP)

def test_initialize_connection(self):
self.flags(lock_path=self.tempdir)
result = self.driver.initialize_connection(self.volume, self.connector)
Expand Down Expand Up @@ -729,14 +796,6 @@ def fake_create_3par_iscsi_host(self, hostname, iscsi_iqn,
self._hosts[hostname] = host
return hostname

def test_create_volume(self):
self.flags(lock_path=self.tempdir)
model_update = self.driver.create_volume(self.volume)
metadata = model_update['metadata']
self.assertFalse(metadata['3ParName'] is None)
self.assertEqual(metadata['CPG'], HP3PAR_CPG)
self.assertEqual(metadata['snapCPG'], HP3PAR_CPG_SNAP)

def test_initialize_connection(self):
self.flags(lock_path=self.tempdir)
result = self.driver.initialize_connection(self.volume, self.connector)
Expand Down
131 changes: 125 additions & 6 deletions cinder/volume/drivers/san/hp/hp_3par_common.py
Expand Up @@ -122,7 +122,8 @@ class HP3PARCommon(object):
'9 - EGENERA',
'10 - ONTAP-legacy',
'11 - VMware']
hp3par_valid_keys = ['cpg', 'snap_cpg', 'provisioning', 'persona']
hp_qos_keys = ['maxIOPS', 'maxBWS']
hp3par_valid_keys = ['cpg', 'snap_cpg', 'provisioning', 'persona', 'vvs']

def __init__(self, config):
self.sshpool = None
Expand Down Expand Up @@ -182,7 +183,8 @@ def get_domain(self, cpg_name):
try:
cpg = self.client.getCPG(cpg_name)
except hpexceptions.HTTPNotFound:
err = (_("CPG (%s) doesn't exist on array.") % cpg_name)
err = (_("Failed to get domain because CPG (%s) doesn't "
"exist on array.") % cpg_name)
LOG.error(err)
raise exception.InvalidInput(reason=err)

Expand Down Expand Up @@ -214,6 +216,10 @@ def _get_3par_snap_name(self, snapshot_id):
snapshot_name = self._encode_name(snapshot_id)
return "oss-%s" % snapshot_name

def _get_3par_vvs_name(self, volume_id):
vvs_name = self._encode_name(volume_id)
return "vvs-%s" % vvs_name

def _encode_name(self, name):
uuid_str = name.replace("-", "")
vol_uuid = uuid.UUID('urn:uuid:%s' % uuid_str)
Expand Down Expand Up @@ -521,6 +527,7 @@ def _update_volume_stats(self):
'reserved_percentage': 0,
'storage_protocol': None,
'total_capacity_gb': 'unknown',
'QoS_support': True,
'vendor_name': 'Hewlett-Packard',
'volume_backend_name': None}

Expand Down Expand Up @@ -569,6 +576,23 @@ def _get_key_value(self, hp3par_keys, key, default=None):
else:
return default

def _get_qos_value(self, qos, key, default=None):
if key in qos:
return qos[key]
else:
return default

def _get_qos_by_volume_type(self, volume_type):
qos = {}
specs = volume_type.get('extra_specs')
for key, value in specs.iteritems():
if 'qos:' in key:
fields = key.split(':')
key = fields[1]
if key in self.hp_qos_keys:
qos[key] = int(value)
return qos

def _get_keys_by_volume_type(self, volume_type):
hp3par_keys = {}
specs = volume_type.get('extra_specs')
Expand All @@ -580,6 +604,40 @@ def _get_keys_by_volume_type(self, volume_type):
hp3par_keys[key] = value
return hp3par_keys

def _set_qos_rule(self, qos, vvs_name):
max_io = self._get_qos_value(qos, 'maxIOPS')
max_bw = self._get_qos_value(qos, 'maxBWS')
cli_qos_string = ""
if max_io is not None:
cli_qos_string += ('-io %s ' % max_io)
if max_bw is not None:
cli_qos_string += ('-bw %sM ' % max_bw)
self._cli_run('setqos %svvset:%s' %
(cli_qos_string, vvs_name), None)

def _add_volume_to_volume_set(self, volume, volume_name,
cpg, vvs_name, qos):
if vvs_name is not None:
# Admin has set a volume set name to add the volume to
self._cli_run('createvvset -add %s %s' % (vvs_name,
volume_name), None)
else:
vvs_name = self._get_3par_vvs_name(volume['id'])
domain = self.get_domain(cpg)
self._cli_run('createvvset -domain %s %s' % (domain,
vvs_name), None)
self._set_qos_rule(qos, vvs_name)
self._cli_run('createvvset -add %s %s' % (vvs_name,
volume_name), None)

def _remove_volume_set(self, vvs_name):
# Must first clear the QoS rules before removing the volume set
self._cli_run('setqos -clear vvset:%s' % (vvs_name), None)
self._cli_run('removevvset -f %s' % (vvs_name), None)

def _remove_volume_from_volume_set(self, volume_name, vvs_name):
self._cli_run('removevvset -f %s %s' % (vvs_name, volume_name), None)

def get_persona_type(self, volume, hp3par_keys=None):
default_persona = self.valid_persona_values[0]
type_id = volume.get('volume_type_id', None)
Expand Down Expand Up @@ -616,11 +674,19 @@ def create_volume(self, volume):

# get the options supported by volume types
volume_type = None
vvs_name = None
hp3par_keys = {}
qos = {}
qos_on_volume = False
type_id = volume.get('volume_type_id', None)
if type_id is not None:
volume_type = self._get_volume_type(type_id)
hp3par_keys = self._get_keys_by_volume_type(volume_type)
vvs_name = self._get_key_value(hp3par_keys, 'vvs')
if vvs_name is None:
qos = self._get_qos_by_volume_type(volume_type)
if qos:
qos_on_volume = True

cpg = self._get_key_value(hp3par_keys, 'cpg',
self.config.hp3par_cpg)
Expand Down Expand Up @@ -665,6 +731,10 @@ def create_volume(self, volume):
if type_id is not None:
comments['volume_type_name'] = volume_type.get('name')
comments['volume_type_id'] = type_id
if vvs_name is not None:
comments['vvs'] = vvs_name
else:
comments['qos'] = qos

extras = {'comment': json.dumps(comments),
'snapCPG': snap_cpg,
Expand All @@ -673,7 +743,15 @@ def create_volume(self, volume):
capacity = self._capacity_from_size(volume['size'])
volume_name = self._get_3par_vol_name(volume['id'])
self.client.createVolume(volume_name, cpg, capacity, extras)

if qos or vvs_name is not None:
try:
self._add_volume_to_volume_set(volume, volume_name,
cpg, vvs_name, qos)
except Exception as ex:
# Delete the volume if unable to add it to the volume set
self.client.deleteVolume(volume_name)
LOG.error(str(ex))
raise exception.CinderException(ex.get_description())
except hpexceptions.HTTPConflict:
raise exception.Duplicate(_("Volume (%s) already exists on array")
% volume_name)
Expand All @@ -688,7 +766,8 @@ def create_volume(self, volume):
raise exception.CinderException(ex.get_description())

metadata = {'3ParName': volume_name, 'CPG': cpg,
'snapCPG': extras['snapCPG']}
'snapCPG': extras['snapCPG'], 'qos': qos_on_volume,
'vvs': vvs_name}
return metadata

def _copy_volume(self, src_name, dest_name):
Expand Down Expand Up @@ -767,6 +846,12 @@ def create_cloned_volume(self, volume, src_vref):
def delete_volume(self, volume):
try:
volume_name = self._get_3par_vol_name(volume['id'])
qos = self.get_volume_metadata_value(volume, 'qos')
vvs_name = self.get_volume_metadata_value(volume, 'vvs')
if vvs_name is not None:
self._remove_volume_from_volume_set(volume_name, vvs_name)
elif qos:
self._remove_volume_set(self._get_3par_vvs_name(volume['id']))
self.client.deleteVolume(volume_name)
except hpexceptions.HTTPNotFound as ex:
# We'll let this act as if it worked
Expand Down Expand Up @@ -797,10 +882,26 @@ def create_volume_from_snapshot(self, volume, snapshot):

try:
snap_name = self._get_3par_snap_name(snapshot['id'])
vol_name = self._get_3par_vol_name(volume['id'])
volume_name = self._get_3par_vol_name(volume['id'])

extra = {'volume_id': volume['id'],
'snapshot_id': snapshot['id']}

volume_type = None
type_id = volume.get('volume_type_id', None)
vvs_name = None
qos = {}
qos_on_volume = False
hp3par_keys = {}
if type_id is not None:
volume_type = self._get_volume_type(type_id)
hp3par_keys = self._get_keys_by_volume_type(volume_type)
vvs_name = self._get_key_value(hp3par_keys, 'vvs')
if vvs_name is None:
qos = self._get_qos_by_volume_type(volume_type)
if qos:
qos_on_volume = True

name = snapshot.get('display_name', None)
if name:
extra['name'] = name
Expand All @@ -812,11 +913,29 @@ def create_volume_from_snapshot(self, volume, snapshot):
optional = {'comment': json.dumps(extra),
'readOnly': False}

self.client.createSnapshot(vol_name, snap_name, optional)
self.client.createSnapshot(volume_name, snap_name, optional)
if qos or vvs_name is not None:
cpg = 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)
except Exception as ex:
# Delete the volume if unable to add it to the volume set
self.client.deleteVolume(volume_name)
LOG.error(str(ex))
raise exception.CinderException(ex.get_description())
except hpexceptions.HTTPForbidden:
raise exception.NotAuthorized()
except hpexceptions.HTTPNotFound:
raise exception.NotFound()
except Exception as ex:
LOG.error(str(ex))
raise exception.CinderException(ex.get_description())

metadata = {'3ParName': volume_name, 'qos': qos_on_volume,
'vvs': vvs_name}
return metadata

def create_snapshot(self, snapshot):
LOG.debug("Create Snapshot\n%s" % pprint.pformat(snapshot))
Expand Down
3 changes: 2 additions & 1 deletion cinder/volume/drivers/san/hp/hp_3par_fc.py
Expand Up @@ -115,8 +115,9 @@ def create_volume_from_snapshot(self, volume, snapshot):
TODO: support using the size from the user.
"""
self.common.client_login()
self.common.create_volume_from_snapshot(volume, snapshot)
metadata = self.common.create_volume_from_snapshot(volume, snapshot)
self.common.client_logout()
return {'metadata': metadata}

@utils.synchronized('3par', external=True)
def create_snapshot(self, snapshot):
Expand Down

0 comments on commit 767a018

Please sign in to comment.