]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Update 3PAR driver
authorKurt Martin <kurt.f.martin@hp.com>
Sat, 12 Jan 2013 01:13:43 +0000 (17:13 -0800)
committerKurt Martin <kurt.f.martin@hp.com>
Fri, 1 Feb 2013 18:44:13 +0000 (10:44 -0800)
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

cinder/tests/test_hp3par_iscsi.py
cinder/volume/drivers/san/hp/hp_3par_common.py
cinder/volume/drivers/san/hp/hp_3par_iscsi.py

index 637038e0aa000356057bf7a007759e2d573b7f27..d1c0ae03b78e8e44dfacd1a3cf8b2dd4981dcf6b 100644 (file)
@@ -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)
index 177037f5e75db08c56c90c60902ec874983b1b14..3bc5ec5bfc8cf0ca7baf7415de538e9de7fcc466 100644 (file)
@@ -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))
index 041ca637c20b9c47ed973bcbad399d28a5a54638..9548b4f4e25fcc2f9a11283776e0fd714a66ff7b 100644 (file)
@@ -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):