From: Kurt Martin Date: Sat, 12 Jan 2013 01:13:43 +0000 (-0800) Subject: Update 3PAR driver X-Git-Url: https://review.fuel-infra.org/gitweb?a=commitdiff_plain;h=da25b7b86dcae00cb380ce5fb9a208b8f7a171a3;p=openstack-build%2Fcinder-build.git Update 3PAR driver Added support for get_volume_stats() Add sanity checks for: Ensure the CPG lives in the Domain that's configured. On 3PAR systems, the create volume from snapshot has to be the same size. The driver now checks to make sure that they are the same size. Now using volume and snapshot id's instead of the names. Checking for optional fields before using them(i.e. description). Added a new method get_ports() to gather the active array ports. Fixed inline comments, added a space between the comment and # sign Change-Id: Ie2aed38c6349bb5ee8bbea4d0928cd606427a26f --- diff --git a/cinder/tests/test_hp3par_iscsi.py b/cinder/tests/test_hp3par_iscsi.py index 637038e0a..d1c0ae03b 100644 --- a/cinder/tests/test_hp3par_iscsi.py +++ b/cinder/tests/test_hp3par_iscsi.py @@ -54,7 +54,8 @@ class FakeHP3ParClient(object): 'usedMiB': 256}, 'SDGrowth': {'LDLayout': {'RAIDType': 4, 'diskPatterns': [{'diskType': 2}]}, - 'incrementMiB': 32768}, + 'incrementMiB': 32768, + 'limitMiB': 1024000}, 'SDUsage': {'rawTotalMiB': 49152, 'rawUsedMiB': 1023, 'totalMiB': 36864, @@ -251,10 +252,12 @@ class FakeHP3ParClient(object): class TestHP3PARDriver(test.TestCase): TARGET_IQN = "iqn.2000-05.com.3pardata:21810002ac00383d" + VOLUME_ID = "d03338a9-9115-48a3-8dfc-35cdfcdc15a7" VOLUME_NAME = "volume-d03338a9-9115-48a3-8dfc-35cdfcdc15a7" + SNAPSHOT_ID = "2f823bdc-e36e-4dc8-bd15-de1c7a28ff31" SNAPSHOT_NAME = "snapshot-2f823bdc-e36e-4dc8-bd15-de1c7a28ff31" VOLUME_3PAR_NAME = "osv-0DM4qZEVSKON-DXN-NwVpw" - SNAPSHOT_VOL_NAME = "oss-L4I73ONuTci9Fd4ceij-MQ" + SNAPSHOT_3PAR_NAME = "oss-L4I73ONuTci9Fd4ceij-MQ" FAKE_HOST = "fakehost" _hosts = {} @@ -292,31 +295,35 @@ class TestHP3PARDriver(test.TestCase): self.fake_delete_3par_host) self.stubs.Set(hpdriver.HP3PARCommon, "_create_3par_vlun", self.fake_create_3par_vlun) + self.stubs.Set(hpdriver.HP3PARCommon, "get_ports", + self.fake_get_ports) self.driver = hpdriver.HP3PARISCSIDriver() self.driver.do_setup(None) self.volume = {'name': self.VOLUME_NAME, + 'id': self.VOLUME_ID, 'display_name': 'Foo Volume', - 'size': 1, + 'size': 2, 'host': self.FAKE_HOST} user_id = '2689d9a913974c008b1d859013f23607' project_id = 'fac88235b9d64685a3530f73e490348f' volume_id = '761fc5e5-5191-4ec7-aeba-33e36de44156' fake_desc = 'test description name' - self.snapshot = type('snapshot', - (object,), - {'name': self.SNAPSHOT_NAME, - 'user_id': user_id, - 'project_id': project_id, - 'volume_id': volume_id, - 'volume_name': self.VOLUME_NAME, - 'status': 'creating', - 'progress': '0%', - 'volume_size': 2, - 'display_name': 'fakesnap', - 'display_description': fake_desc})() + fake_fc_ports = ['0987654321234', '123456789000987'] + fake_iscsi_ports = ['10.10.10.10', '10.10.10.11'] + self.snapshot = {'name': self.SNAPSHOT_NAME, + 'id': self.SNAPSHOT_ID, + 'user_id': user_id, + 'project_id': project_id, + 'volume_id': volume_id, + 'volume_name': self.VOLUME_NAME, + 'status': 'creating', + 'progress': '0%', + 'volume_size': 2, + 'display_name': 'fakesnap', + 'display_description': fake_desc} self.connector = {'ip': '10.0.0.2', 'initiator': 'iqn.1993-08.org.debian:01:222', 'host': 'fakehost'} @@ -328,6 +335,13 @@ class TestHP3PARDriver(test.TestCase): 'target_lun': 186, 'target_portal': '1.1.1.2:1234'}, 'driver_volume_type': 'iscsi'} + self.stats = {'driver_version': '1.0', + 'free_capacity_gb': 968, + 'reserved_percentage': 0, + 'storage_protocol': 'iSCSI', + 'total_capacity_gb': 1000, + 'vendor_name': 'Hewlett-Packard', + 'volume_backend_name': 'HP3PARISCSIDriver'} def tearDown(self): shutil.rmtree(self.tempdir) @@ -375,6 +389,9 @@ class TestHP3PARDriver(test.TestCase): def fake_create_3par_vlun(self, volume, hostname): self.driver.client.createVLUN(volume, 19, hostname) + def fake_get_ports(self): + return {'FC': self.fake_fc_ports, 'iSCSI': self.fake_iscsi_ports} + def test_create_volume(self): self.flags(lock_path=self.tempdir) model_update = self.driver.create_volume(self.volume) @@ -389,13 +406,29 @@ class TestHP3PARDriver(test.TestCase): self.driver.client.getVolume, self.VOLUME_NAME) + def test_get_volume_stats(self): + self.flags(lock_path=self.tempdir) + vol_stats = self.driver.get_volume_stats(True) + self.assertEqual(vol_stats['driver_version'], + self.stats['driver_version']) + self.assertEqual(vol_stats['free_capacity_gb'], + self.stats['free_capacity_gb']) + self.assertEqual(vol_stats['reserved_percentage'], + self.stats['reserved_percentage']) + self.assertEqual(vol_stats['storage_protocol'], + self.stats['storage_protocol']) + self.assertEqual(vol_stats['vendor_name'], + self.stats['vendor_name']) + self.assertEqual(vol_stats['volume_backend_name'], + self.stats['volume_backend_name']) + def test_create_snapshot(self): self.flags(lock_path=self.tempdir) self.driver.create_snapshot(self.snapshot) # check to see if the snapshot was created - snap_vol = self.driver.client.getVolume(self.SNAPSHOT_VOL_NAME) - self.assertEqual(snap_vol['name'], self.SNAPSHOT_VOL_NAME) + snap_vol = self.driver.client.getVolume(self.SNAPSHOT_3PAR_NAME) + self.assertEqual(snap_vol['name'], self.SNAPSHOT_3PAR_NAME) def test_delete_snapshot(self): self.flags(lock_path=self.tempdir) @@ -404,14 +437,14 @@ class TestHP3PARDriver(test.TestCase): # the snapshot should be deleted now self.assertRaises(hpexceptions.HTTPNotFound, self.driver.client.getVolume, - self.SNAPSHOT_VOL_NAME) + self.SNAPSHOT_NAME) def test_create_volume_from_snapshot(self): self.flags(lock_path=self.tempdir) self.driver.create_volume_from_snapshot(self.volume, self.snapshot) - snap_vol = self.driver.client.getVolume(self.SNAPSHOT_VOL_NAME) - self.assertEqual(snap_vol['name'], self.SNAPSHOT_VOL_NAME) + snap_vol = self.driver.client.getVolume(self.SNAPSHOT_3PAR_NAME) + self.assertEqual(snap_vol['name'], self.SNAPSHOT_3PAR_NAME) def test_initialize_connection(self): self.flags(lock_path=self.tempdir) diff --git a/cinder/volume/drivers/san/hp/hp_3par_common.py b/cinder/volume/drivers/san/hp/hp_3par_common.py index 177037f5e..3bc5ec5bf 100644 --- a/cinder/volume/drivers/san/hp/hp_3par_common.py +++ b/cinder/volume/drivers/san/hp/hp_3par_common.py @@ -93,6 +93,14 @@ FLAGS.register_opts(hp3par_opts) class HP3PARCommon(): + stats = {'driver_version': '1.0', + 'free_capacity_gb': 'unknown', + 'reserved_percentage': 0, + 'storage_protocol': None, + 'total_capacity_gb': 'unknown', + 'vendor_name': 'Hewlett-Packard', + 'volume_backend_name': None} + def __init__(self): self.sshpool = None @@ -101,10 +109,10 @@ class HP3PARCommon(): if not getattr(FLAGS, flag, None): raise exception.InvalidInput(reason=_('%s is not set') % flag) - def _get_3par_vol_name(self, name): + def _get_3par_vol_name(self, volume_id): """ - Converts the openstack volume name from - volume-ecffc30f-98cb-4cf5-85ee-d7309cc17cd2 + Converts the openstack volume id from + ecffc30f-98cb-4cf5-85ee-d7309cc17cd2 to osv-7P.DD5jLTPWF7tcwnMF80g @@ -115,13 +123,11 @@ class HP3PARCommon(): We strip the padding '=' and replace + with . and / with - """ - name = name.replace("volume-", "") - volume_name = self._encode_name(name) + volume_name = self._encode_name(volume_id) return "osv-%s" % volume_name - def _get_3par_snap_name(self, name): - name = name.replace("snapshot-", "") - snapshot_name = self._encode_name(name) + def _get_3par_snap_name(self, snapshot_id): + snapshot_name = self._encode_name(snapshot_id) return "oss-%s" % snapshot_name def _encode_name(self, name): @@ -132,7 +138,7 @@ class HP3PARCommon(): # 3par doesn't allow +, nor / vol_encoded = vol_encoded.replace('+', '.') vol_encoded = vol_encoded.replace('/', '-') - #strip off the == as 3par doesn't like those. + # strip off the == as 3par doesn't like those. vol_encoded = vol_encoded.replace('=', '') return vol_encoded @@ -192,8 +198,8 @@ class HP3PARCommon(): exit ''' % cmd) - #stdin.write('process_input would go here') - #stdin.flush() + # stdin.write('process_input would go here') + # stdin.flush() # NOTE(justinsb): This seems suspicious... # ...other SSH clients have buffering issues with this approach @@ -261,7 +267,7 @@ exit # couldn't find it index = len(hostname) - #we'll just chop this off for now. + # we'll just chop this off for now. if index > 23: index = 23 @@ -353,19 +359,80 @@ exit return host + def get_ports(self): + # First get the active FC ports + out = self._cli_run('showport', None) + + # strip out header + # N:S:P,Mode,State,----Node_WWN----,-Port_WWN/HW_Addr-,Type, + # Protocol,Label,Partner,FailoverState + out = out[1:len(out) - 2] + + ports = {'FC': [], 'iSCSI': []} + for line in out: + tmp = line.split(',') + + if tmp: + if tmp[1] == 'target' and tmp[2] == 'ready': + if tmp[6] == 'FC': + port = {'wwn': tmp[4], 'nsp': tmp[0]} + ports['FC'].append(port) + + # now get the active iSCSI ports + out = self._cli_run('showport -iscsi', None) + + # strip out header + # N:S:P,State,IPAddr,Netmask,Gateway, + # TPGT,MTU,Rate,DHCP,iSNS_Addr,iSNS_Port + out = out[1:len(out) - 2] + for line in out: + tmp = line.split(',') + + if tmp: + if tmp[1] == 'ready': + port = {'ip': tmp[2], 'nsp': tmp[0]} + ports['iSCSI'].append(port) + + LOG.debug("PORTS = %s" % pprint.pformat(ports)) + return ports + + def get_volume_stats(self, refresh, client): + # const to convert MiB to GB + const = 0.0009765625 + + if refresh: + try: + cpg = client.getCPG(FLAGS.hp3par_cpg) + if 'limitMiB' not in cpg['SDGrowth']: + total_capacity = 'infinite' + free_capacity = 'infinite' + else: + total_capacity = int(cpg['SDGrowth']['limitMiB'] * const) + free_capacity = int((cpg['SDGrowth']['limitMiB'] - + cpg['UsrUsage']['usedMiB']) * const) + + self.stats['total_capacity_gb'] = total_capacity + self.stats['free_capacity_gb'] = free_capacity + except hpexceptions.HTTPNotFound: + err = _("CPG (%s) doesn't exist on array") % FLAGS.hp3par_cpg + LOG.error(err) + raise exception.InvalidInput(reason=err) + + return self.stats + def create_vlun(self, volume, host, client): """ In order to export a volume on a 3PAR box, we have to create a VLUN. """ - volume_name = self._get_3par_vol_name(volume['name']) + volume_name = self._get_3par_vol_name(volume['id']) self._create_3par_vlun(volume_name, host['name']) return client.getVLUN(volume_name) def delete_vlun(self, volume, connector, client): hostname = self._safe_hostname(connector['host']) - volume_name = self._get_3par_vol_name(volume['name']) + volume_name = self._get_3par_vol_name(volume['id']) vlun = client.getVLUN(volume_name) client.deleteVLUN(volume_name, vlun['lun'], hostname) self._delete_3par_host(hostname) @@ -375,11 +442,16 @@ exit """ Create a new volume """ LOG.debug("CREATE VOLUME (%s : %s %s)" % (volume['display_name'], volume['name'], - self._get_3par_vol_name(volume['name']))) + self._get_3par_vol_name(volume['id']))) try: - comments = {'name': volume['name'], - 'display_name': volume['display_name'], + comments = {'volume_id': volume['id'], + 'name': volume['name'], 'type': 'OpenStack'} + + name = volume.get('display_name', None) + if name: + comments['display_name'] = name + extras = {'comment': json.dumps(comments), 'snapCPG': FLAGS.hp3par_cpg_snap} @@ -387,7 +459,7 @@ exit extras['snapCPG'] = FLAGS.hp3par_cpg capacity = self._capacity_from_size(volume['size']) - volume_name = self._get_3par_vol_name(volume['name']) + volume_name = self._get_3par_vol_name(volume['id']) client.createVolume(volume_name, FLAGS.hp3par_cpg, capacity, extras) @@ -401,15 +473,20 @@ exit LOG.error(str(ex)) raise exception.CinderException(ex.get_description()) + metadata = {'3ParName': volume_name, 'CPG': FLAGS.hp3par_cpg, + 'snapCPG': extras['snapCPG']} + return metadata + @lockutils.synchronized('3par', 'cinder-', True) def delete_volume(self, volume, client): """ Delete a volume """ try: - volume_name = self._get_3par_vol_name(volume['name']) + volume_name = self._get_3par_vol_name(volume['id']) client.deleteVolume(volume_name) except hpexceptions.HTTPNotFound as ex: + # We'll let this act as if it worked + # it helps clean up the cinder entries. LOG.error(str(ex)) - raise exception.NotFound(ex.get_description()) except hpexceptions.HTTPForbidden as ex: LOG.error(str(ex)) raise exception.NotAuthorized(ex.get_description()) @@ -426,21 +503,35 @@ exit """ LOG.debug("Create Volume from Snapshot\n%s\n%s" % (pprint.pformat(volume['display_name']), - pprint.pformat(snapshot.display_name))) + pprint.pformat(snapshot['display_name']))) + + if snapshot['volume_size'] != volume['size']: + err = "You cannot change size of the volume. It must \ +be the same as it's Snapshot." + LOG.error(err) + raise exception.InvalidInput(reason=err) + try: - snap_name = self._get_3par_snap_name(snapshot.name) - vol_name = self._get_3par_vol_name(volume['name']) + snap_name = self._get_3par_snap_name(snapshot['id']) + vol_name = self._get_3par_vol_name(volume['id']) + + extra = {'volume_id': volume['id'], + 'snapshot_id': snapshot['id']} + name = snapshot.get('display_name', None) + if name: + extra['name'] = name - extra = {'name': snapshot.display_name, - 'description': snapshot.display_description} + description = snapshot.get('display_description', None) + if description: + extra['description'] = description optional = {'comment': json.dumps(extra), 'readOnly': False} client.createSnapshot(vol_name, snap_name, optional) - except hpexceptions.HTTPForbidden as ex: + except hpexceptions.HTTPForbidden: raise exception.NotAuthorized() - except hpexceptions.HTTPNotFound as ex: + except hpexceptions.HTTPNotFound: raise exception.NotFound() @lockutils.synchronized('3par', 'cinder-', True) @@ -449,12 +540,23 @@ exit LOG.debug("Create Snapshot\n%s" % pprint.pformat(snapshot)) try: - snap_name = self._get_3par_snap_name(snapshot.name) - vol_name = self._get_3par_vol_name(snapshot.volume_name) + snap_name = self._get_3par_snap_name(snapshot['id']) + vol_name = self._get_3par_vol_name(snapshot['volume_id']) + + extra = {'volume_name': snapshot['volume_name']} + vol_id = snapshot.get('volume_id', None) + if vol_id: + extra['volume_id'] = vol_id - extra = {'name': snapshot.display_name, - 'vol_name': snapshot.volume_name, - 'description': snapshot.display_description} + try: + extra['name'] = snapshot['display_name'] + except AttribteError: + pass + + try: + extra['description'] = snapshot['display_description'] + except AttribteError: + pass optional = {'comment': json.dumps(extra), 'readOnly': True} @@ -476,9 +578,9 @@ exit LOG.debug("Delete Snapshot\n%s" % pprint.pformat(snapshot)) try: - snap_name = self._get_3par_snap_name(snapshot.name) + snap_name = self._get_3par_snap_name(snapshot['id']) client.deleteVolume(snap_name) except hpexceptions.HTTPForbidden: raise exception.NotAuthorized() - except hpexceptions.HTTPNotFound: - raise exception.NotFound() + except hpexceptions.HTTPNotFound as ex: + LOG.error(str(ex)) diff --git a/cinder/volume/drivers/san/hp/hp_3par_iscsi.py b/cinder/volume/drivers/san/hp/hp_3par_iscsi.py index 041ca637c..9548b4f4e 100644 --- a/cinder/volume/drivers/san/hp/hp_3par_iscsi.py +++ b/cinder/volume/drivers/san/hp/hp_3par_iscsi.py @@ -59,6 +59,12 @@ class HP3PARISCSIDriver(cinder.volume.driver.ISCSIDriver): def _create_client(self): return client.HP3ParClient(FLAGS.hp3par_api_url) + def get_volume_stats(self, refresh): + stats = self.common.get_volume_stats(refresh, self.client) + stats['storage_protocol'] = 'iSCSI' + stats['volume_backend_name'] = 'HP3PARISCSIDriver' + return stats + def do_setup(self, context): self.common = self._init_common() self._check_flags() @@ -77,12 +83,21 @@ class HP3PARISCSIDriver(cinder.volume.driver.ISCSIDriver): # make sure the CPG exists try: - self.client.getCPG(FLAGS.hp3par_cpg) + cpg = self.client.getCPG(FLAGS.hp3par_cpg) except hpexceptions.HTTPNotFound as ex: err = _("CPG (%s) doesn't exist on array") % FLAGS.hp3par_cpg LOG.error(err) raise exception.InvalidInput(reason=err) + if 'domain' not in cpg and cpg['domain'] != FLAGS.hp3par_domain: + err = "CPG's domain '%s' and config option hp3par_domain '%s' \ +must be the same" % (cpg['domain'], FLAGS.hp3par_domain) + LOG.error(err) + raise exception.InvalidInput(reason=err) + + # make sure ssh works. + self._iscsi_discover_target_iqn(FLAGS.iscsi_ip_address) + def check_for_setup_error(self): """Returns an error if prerequisites aren't met.""" self._check_flags() @@ -90,10 +105,11 @@ class HP3PARISCSIDriver(cinder.volume.driver.ISCSIDriver): @lockutils.synchronized('3par-vol', 'cinder-', True) def create_volume(self, volume): """ Create a new volume """ - self.common.create_volume(volume, self.client, FLAGS) + metadata = self.common.create_volume(volume, self.client, FLAGS) return {'provider_location': "%s:%s" % - (FLAGS.iscsi_ip_address, FLAGS.iscsi_port)} + (FLAGS.iscsi_ip_address, FLAGS.iscsi_port), + 'metadata': metadata} @lockutils.synchronized('3par-vol', 'cinder-', True) def delete_volume(self, volume):