]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
HP 3PAR Fibre Channel Driver and iSCSI Updates
authorKurt Martin <kurt.f.martin@hp.com>
Wed, 13 Feb 2013 21:55:43 +0000 (13:55 -0800)
committerKurt Martin <kurt.f.martin@hp.com>
Tue, 19 Feb 2013 17:56:19 +0000 (09:56 -0800)
This is the initial FC driver for the HP 3PAR array, along with some
3PAR iSCSI driver cleanup and changes in the HP 3PAR common class.
Summary of changes:
* Initial FC Driver for the Grizzly release (depends on the FC base class
  and nova FC changes https://review.openstack.org/#/c/19992/ &
  https://review.openstack.org/#/c/20003/)
* Updates to iSCSI Driver include volume type and create clone volume support
  as well as general cleanup(comments, copyright, minor bug fixes).
* The tests have been updated to add FC test and the file was renamed
  to reflect both FC and iSCSI 3PAR tests.
* Volume types added for the Grizzly release include cpg, snap_cpg, persona
  and provisioning(thin vs. full)

Partially Implements: blueprint fibre-channel-block-storage

Change-Id: Ie1c2e755aa1bcd5994ea4b02674718721b958e07

cinder/tests/test_hp3par.py [moved from cinder/tests/test_hp3par_iscsi.py with 62% similarity]
cinder/volume/drivers/san/hp/hp_3par_common.py
cinder/volume/drivers/san/hp/hp_3par_fc.py [new file with mode: 0644]
cinder/volume/drivers/san/hp/hp_3par_iscsi.py

similarity index 62%
rename from cinder/tests/test_hp3par_iscsi.py
rename to cinder/tests/test_hp3par.py
index d1c0ae03b78e8e44dfacd1a3cf8b2dd4981dcf6b..1e51da04981c2b3f7b11e295579c233f545c4923 100644 (file)
@@ -1,7 +1,7 @@
 #!/usr/bin/env python
 # vim: tabstop=4 shiftwidth=4 softtabstop=4
 #
-#    Copyright (c) 2012 Hewlett-Packard, Inc.
+#    (c) Copyright 2013 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.
 """
-Unit tests for OpenStack Cinder volume driver
+Unit tests for OpenStack Cinder volume drivers
 """
 import shutil
 import tempfile
 
 from hp3parclient import exceptions as hpexceptions
 
+from cinder import exception
 import cinder.flags
 from cinder.openstack.common import log as logging
 from cinder import test
+from cinder.volume.drivers.san.hp import hp_3par_fc as hpfcdriver
 from cinder.volume.drivers.san.hp import hp_3par_iscsi as hpdriver
 
 FLAGS = cinder.flags.FLAGS
@@ -54,8 +56,7 @@ class FakeHP3ParClient(object):
                      'usedMiB': 256},
          'SDGrowth': {'LDLayout': {'RAIDType': 4,
                       'diskPatterns': [{'diskType': 2}]},
-                      'incrementMiB': 32768,
-                      'limitMiB': 1024000},
+                      'incrementMiB': 32768},
          'SDUsage': {'rawTotalMiB': 49152,
                      'rawUsedMiB': 1023,
                      'totalMiB': 36864,
@@ -249,22 +250,258 @@ class FakeHP3ParClient(object):
         raise hpexceptions.HTTPNotFound(msg)
 
 
-class TestHP3PARDriver(test.TestCase):
+class HP3PARBaseDriver():
 
-    TARGET_IQN = "iqn.2000-05.com.3pardata:21810002ac00383d"
     VOLUME_ID = "d03338a9-9115-48a3-8dfc-35cdfcdc15a7"
+    CLONE_ID = "d03338a9-9115-48a3-8dfc-000000000000"
     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_3PAR_NAME = "oss-L4I73ONuTci9Fd4ceij-MQ"
     FAKE_HOST = "fakehost"
+    USER_ID = '2689d9a913974c008b1d859013f23607'
+    PROJECT_ID = 'fac88235b9d64685a3530f73e490348f'
+    VOLUME_ID_SNAP = '761fc5e5-5191-4ec7-aeba-33e36de44156'
+    FAKE_DESC = 'test description name'
+    FAKE_FC_PORTS = ['0987654321234', '123456789000987']
+    FAKE_ISCSI_PORTS = ['10.10.10.10', '10.10.10.11']
+
+    volume = {'name': VOLUME_NAME,
+              'id': VOLUME_ID,
+              'display_name': 'Foo Volume',
+              'size': 2,
+              'host': FAKE_HOST,
+              'volume_type': None,
+              'volume_type_id': None}
+
+    snapshot = {'name': SNAPSHOT_NAME,
+                'id': SNAPSHOT_ID,
+                'user_id': USER_ID,
+                'project_id': PROJECT_ID,
+                'volume_id': VOLUME_ID_SNAP,
+                'volume_name': VOLUME_NAME,
+                'status': 'creating',
+                'progress': '0%',
+                'volume_size': 2,
+                'display_name': 'fakesnap',
+                'display_description': FAKE_DESC}
+
+    connector = {'ip': '10.0.0.2',
+                 'initiator': 'iqn.1993-08.org.debian:01:222',
+                 'wwpns': ["123456789012345", "123456789054321"],
+                 'wwnns': ["223456789012345", "223456789054321"],
+                 'host': 'fakehost'}
+
+    def fake_create_client(self):
+        return FakeHP3ParClient(FLAGS.hp3par_api_url)
+
+    def fake_get_3par_host(self, hostname):
+        if hostname not in self._hosts:
+            msg = {'code': 'NON_EXISTENT_HOST',
+                   'desc': "HOST '%s' was not found" % hostname}
+            raise hpexceptions.HTTPNotFound(msg)
+        else:
+            return self._hosts[hostname]
+
+    def fake_delete_3par_host(self, hostname):
+        if hostname not in self._hosts:
+            msg = {'code': 'NON_EXISTENT_HOST',
+                   'desc': "HOST '%s' was not found" % hostname}
+            raise hpexceptions.HTTPNotFound(msg)
+        else:
+            self._hosts[hostname] = None
+
+    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 fake_copy_volume(self, src_name, dest_name):
+        pass
+
+    def fake_get_volume_state(self, vol_name):
+        return "normal"
+
+    def test_delete_volume(self):
+        self.flags(lock_path=self.tempdir)
+        self.driver.delete_volume(self.volume)
+        self.assertRaises(hpexceptions.HTTPNotFound,
+                          self.driver.client.getVolume,
+                          self.VOLUME_ID)
+
+    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_3PAR_NAME)
+        self.assertEqual(snap_vol['name'], self.SNAPSHOT_3PAR_NAME)
+
+    def test_delete_snapshot(self):
+        self.flags(lock_path=self.tempdir)
+        self.driver.delete_snapshot(self.snapshot)
+
+        # the snapshot should be deleted now
+        self.assertRaises(hpexceptions.HTTPNotFound,
+                          self.driver.client.getVolume,
+                          self.SNAPSHOT_3PAR_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_3PAR_NAME)
+        self.assertEqual(snap_vol['name'], self.SNAPSHOT_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)
+        self.driver.terminate_connection(self.volume, self.connector, True)
+        # vlun should be gone.
+        self.assertRaises(hpexceptions.HTTPNotFound,
+                          self.driver.client.getVLUN,
+                          self.VOLUME_3PAR_NAME)
+
+
+class TestHP3PARFCDriver(HP3PARBaseDriver, test.TestCase):
+
+    _hosts = {}
+
+    def setUp(self):
+        self.tempdir = tempfile.mkdtemp()
+        super(TestHP3PARFCDriver, self).setUp()
+        self.flags(
+            hp3par_username='testUser',
+            hp3par_password='testPassword',
+            hp3par_api_url='https://1.1.1.1/api/v1',
+            hp3par_domain=HP3PAR_DOMAIN,
+            hp3par_cpg=HP3PAR_CPG,
+            hp3par_cpg_snap=HP3PAR_CPG_SNAP,
+            iscsi_ip_address='1.1.1.2',
+            iscsi_port='1234',
+            san_ip='2.2.2.2',
+            san_login='test',
+            san_password='test'
+        )
+        self.stubs.Set(hpfcdriver.HP3PARFCDriver, "_create_client",
+                       self.fake_create_client)
+        self.stubs.Set(hpfcdriver.HP3PARFCDriver,
+                       "_create_3par_fibrechan_host",
+                       self.fake_create_3par_fibrechan_host)
+
+        self.stubs.Set(hpfcdriver.HP3PARCommon, "_get_3par_host",
+                       self.fake_get_3par_host)
+        self.stubs.Set(hpfcdriver.HP3PARCommon, "_delete_3par_host",
+                       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 = hpfcdriver.HP3PARFCDriver()
+        self.driver.do_setup(None)
+
+    def tearDown(self):
+        shutil.rmtree(self.tempdir)
+        super(TestHP3PARFCDriver, self).tearDown()
+
+    def fake_create_3par_fibrechan_host(self, hostname, wwn,
+                                        domain, persona_id):
+        host = {'FCPaths': [{'driverVersion': None,
+                             'firmwareVersion': None,
+                             'hostSpeed': 0,
+                             'model': None,
+                             'portPos': {'cardPort': 1, 'node': 1,
+                                         'slot': 2},
+                             'vendor': None,
+                             'wwn': wwn[0]},
+                            {'driverVersion': None,
+                             'firmwareVersion': None,
+                             'hostSpeed': 0,
+                             'model': None,
+                             'portPos': {'cardPort': 1, 'node': 0,
+                                         'slot': 2},
+                             'vendor': None,
+                             'wwn': wwn[1]}],
+                'descriptors': None,
+                'domain': domain,
+                'iSCSIPaths': [],
+                'id': 11,
+                'name': hostname}
+        self._hosts[hostname] = host
+
+        self.properties = {'data':
+                          {'target_discovered': True,
+                           'target_lun': 186,
+                           'target_portal': '1.1.1.2:1234'},
+                           'driver_volume_type': 'fibre_channel'}
+
+    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)
+        self.assertEqual(result['driver_volume_type'], 'fibre_channel')
+
+        # we should have a host and a vlun now.
+        host = self.fake_get_3par_host(self.FAKE_HOST)
+        self.assertEquals(self.FAKE_HOST, host['name'])
+        self.assertEquals(HP3PAR_DOMAIN, host['domain'])
+        vlun = self.driver.client.getVLUN(self.VOLUME_3PAR_NAME)
+
+        self.assertEquals(self.VOLUME_3PAR_NAME, vlun['volumeName'])
+        self.assertEquals(self.FAKE_HOST, vlun['hostname'])
+
+    def test_create_cloned_volume(self):
+        self.flags(lock_path=self.tempdir)
+        self.stubs.Set(hpdriver.HP3PARCommon, "_get_volume_state",
+                       self.fake_get_volume_state)
+        self.stubs.Set(hpdriver.HP3PARCommon, "_copy_volume",
+                       self.fake_copy_volume)
+        self.state_tries = 0
+        volume = {'name': HP3PARBaseDriver.VOLUME_NAME,
+                  'id': HP3PARBaseDriver.CLONE_ID,
+                  'display_name': 'Foo Volume',
+                  'size': 2,
+                  'host': HP3PARBaseDriver.FAKE_HOST,
+                  'source_volid': HP3PARBaseDriver.VOLUME_ID}
+        src_vref = {}
+        model_update = self.driver.create_cloned_volume(volume, src_vref)
+        self.assertTrue(model_update is not None)
+        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_get_volume_stats(self):
+        self.flags(lock_path=self.tempdir)
+        stats = self.driver.get_volume_stats(True)
+        self.assertEquals(stats['storage_protocol'], 'FC')
+        self.assertEquals(stats['volume_backend_name'], 'HP3PARFCDriver')
+
+
+class TestHP3PARISCSIDriver(HP3PARBaseDriver, test.TestCase):
+
+    TARGET_IQN = "iqn.2000-05.com.3pardata:21810002ac00383d"
 
     _hosts = {}
 
     def setUp(self):
         self.tempdir = tempfile.mkdtemp()
-        super(TestHP3PARDriver, self).setUp()
+        super(TestHP3PARISCSIDriver, self).setUp()
         self.flags(
             hp3par_username='testUser',
             hp3par_password='testPassword',
@@ -295,65 +532,27 @@ 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': 2,
-                       'host': self.FAKE_HOST}
-
-        user_id = '2689d9a913974c008b1d859013f23607'
-        project_id = 'fac88235b9d64685a3530f73e490348f'
-        volume_id = '761fc5e5-5191-4ec7-aeba-33e36de44156'
-        fake_desc = 'test description name'
-        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'}
-
         target_iqn = 'iqn.2000-05.com.3pardata:21810002ac00383d'
         self.properties = {'data':
-                           {'target_discovered': True,
-                            'target_iqn': target_iqn,
-                            'target_lun': 186,
-                            'target_portal': '1.1.1.2:1234'},
+                          {'target_discovered': True,
+                           'target_iqn': target_iqn,
+                           '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)
-        super(TestHP3PARDriver, self).tearDown()
-
-    def fake_create_client(self):
-        return FakeHP3ParClient(FLAGS.hp3par_api_url)
+        super(TestHP3PARISCSIDriver, self).tearDown()
 
     def fake_iscsi_discover_target_iqn(self, ip_address):
         return self.TARGET_IQN
 
-    def fake_create_3par_iscsi_host(self, hostname, iscsi_iqn, domain):
+    def fake_create_3par_iscsi_host(self, hostname, iscsi_iqn,
+                                    domain, persona_id):
         host = {'FCPaths': [],
                 'descriptors': None,
                 'domain': domain,
@@ -370,82 +569,20 @@ class TestHP3PARDriver(test.TestCase):
                 'name': hostname}
         self._hosts[hostname] = host
 
-    def fake_get_3par_host(self, hostname):
-        if hostname not in self._hosts:
-            msg = {'code': 'NON_EXISTENT_HOST',
-                   'desc': "HOST '%s' was not found" % hostname}
-            raise hpexceptions.HTTPNotFound(msg)
-        else:
-            return self._hosts[hostname]
-
-    def fake_delete_3par_host(self, hostname):
-        if hostname not in self._hosts:
-            msg = {'code': 'NON_EXISTENT_HOST',
-                   'desc': "HOST '%s' was not found" % hostname}
-            raise hpexceptions.HTTPNotFound(msg)
-        else:
-            self._hosts[hostname] = None
-
-    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 fake_iscsi_discover_target_iqn(self, remote_ip):
+        return 'iqn.2000-05.com.3pardata:21810002ac00383d'
 
     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)
         expected_location = "%s:%s" % (FLAGS.iscsi_ip_address,
                                        FLAGS.iscsi_port)
         self.assertEqual(model_update['provider_location'], expected_location)
 
-    def test_delete_volume(self):
-        self.flags(lock_path=self.tempdir)
-        self.driver.delete_volume(self.volume)
-        self.assertRaises(hpexceptions.HTTPNotFound,
-                          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_3PAR_NAME)
-        self.assertEqual(snap_vol['name'], self.SNAPSHOT_3PAR_NAME)
-
-    def test_delete_snapshot(self):
-        self.flags(lock_path=self.tempdir)
-        self.driver.delete_snapshot(self.snapshot)
-
-        # the snapshot should be deleted now
-        self.assertRaises(hpexceptions.HTTPNotFound,
-                          self.driver.client.getVolume,
-                          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_3PAR_NAME)
-        self.assertEqual(snap_vol['name'], self.SNAPSHOT_3PAR_NAME)
-
     def test_initialize_connection(self):
         self.flags(lock_path=self.tempdir)
         result = self.driver.initialize_connection(self.volume, self.connector)
@@ -466,11 +603,29 @@ class TestHP3PARDriver(test.TestCase):
         self.assertEquals(self.VOLUME_3PAR_NAME, vlun['volumeName'])
         self.assertEquals(self.FAKE_HOST, vlun['hostname'])
 
-    def test_terminate_connection(self):
+    def test_create_cloned_volume(self):
         self.flags(lock_path=self.tempdir)
-        self.driver.terminate_connection(self.volume,
-                                         self.connector, True)
-        # vlun should be gone.
-        self.assertRaises(hpexceptions.HTTPNotFound,
-                          self.driver.client.getVLUN,
-                          self.VOLUME_3PAR_NAME)
+        self.stubs.Set(hpdriver.HP3PARCommon, "_get_volume_state",
+                       self.fake_get_volume_state)
+        self.stubs.Set(hpdriver.HP3PARCommon, "_copy_volume",
+                       self.fake_copy_volume)
+        self.state_tries = 0
+        volume = {'name': HP3PARBaseDriver.VOLUME_NAME,
+                  'id': HP3PARBaseDriver.CLONE_ID,
+                  'display_name': 'Foo Volume',
+                  'size': 2,
+                  'host': HP3PARBaseDriver.FAKE_HOST,
+                  'source_volid': HP3PARBaseDriver.VOLUME_ID}
+        src_vref = {}
+        model_update = self.driver.create_cloned_volume(volume, src_vref)
+        self.assertTrue(model_update is not None)
+        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_get_volume_stats(self):
+        self.flags(lock_path=self.tempdir)
+        stats = self.driver.get_volume_stats(True)
+        self.assertEquals(stats['storage_protocol'], 'iSCSI')
+        self.assertEquals(stats['volume_backend_name'], 'HP3PARISCSIDriver')
index e981f755845c7e8a565d4e987e2aae1a5da59999..7203f299a057db9f9a0e3af964986b67e7e9cbf8 100644 (file)
@@ -1,6 +1,6 @@
 # vim: tabstop=4 shiftwidth=4 softtabstop=4
 #
-#    Copyright (c) 2012 Hewlett-Packard, Inc.
+#    (c) Copyright 2012-2013 Hewlett-Packard Development Company, L.P.
 #    All Rights Reserved.
 #
 #    Copyright 2012 OpenStack LLC
 #
 """
 Volume driver common utilities for HP 3PAR Storage array
-This driver requires 3.1.2 firmware on the 3PAR array.
+The 3PAR drivers requires 3.1.2 firmware on the 3PAR array.
 
-The driver uses both the REST service and the SSH
+The drivers uses both the REST service and the SSH
 command line to correctly operate.  Since the
 ssh credentials and the REST credentials can be different
 we need to have settings for both.
 
-This driver requires the use of the san_ip, san_login,
+The drivers requires the use of the san_ip, san_login,
 san_password settings for ssh connections into the 3PAR
 array.   It also requires the setting of
 hp3par_api_url, hp3par_username, hp3par_password
@@ -38,17 +38,20 @@ import json
 import paramiko
 import pprint
 from random import randint
+import time
 import uuid
 
 from eventlet import greenthread
 from hp3parclient import exceptions as hpexceptions
 
+from cinder import context
 from cinder import exception
 from cinder import flags
 from cinder.openstack.common import cfg
 from cinder.openstack.common import lockutils
 from cinder.openstack.common import log as logging
 from cinder import utils
+from cinder.volume import volume_types
 
 
 LOG = logging.getLogger(__name__)
@@ -102,6 +105,18 @@ class HP3PARCommon():
              'vendor_name': 'Hewlett-Packard',
              'volume_backend_name': None}
 
+    # Valid values for volume type extra specs
+    # The first value in the list is the default value
+    valid_prov_values = ['thin', 'full']
+    valid_persona_values = ['1 - Generic',
+                            '2 - Generic-ALUA',
+                            '6 - Generic-legacy',
+                            '7 - HPUX-legacy',
+                            '8 - AIX-legacy',
+                            '9 - EGENERA',
+                            '10 - ONTAP-legacy',
+                            '11 - VMWare']
+
     def __init__(self):
         self.sshpool = None
 
@@ -159,7 +174,7 @@ class HP3PARCommon():
         return capacity
 
     def _cli_run(self, verb, cli_args):
-        """Runs a CLI command over SSH, without doing any result parsing"""
+        """ Runs a CLI command over SSH, without doing any result parsing. """
         cli_arg_strings = []
         if cli_args:
             for k, v in cli_args.items():
@@ -185,7 +200,7 @@ class HP3PARCommon():
         from the CLI command.   We first have to issue
         a command to tell the CLI that we want the output
         to be formatted in CSV, then we issue the real
-        command
+        command.
         """
         LOG.debug(_('Running cmd (SSH): %s'), cmd)
 
@@ -260,7 +275,7 @@ exit
     def _safe_hostname(self, hostname):
         """
         We have to use a safe hostname length
-        for 3PAR host names
+        for 3PAR host names.
         """
         try:
             index = hostname.index('.')
@@ -376,8 +391,7 @@ exit
             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)
+                        ports['FC'].append(tmp[4])
 
         # now get the active iSCSI ports
         out = self._cli_run('showport -iscsi', None)
@@ -391,8 +405,7 @@ exit
 
             if tmp:
                 if tmp[1] == 'ready':
-                    port = {'ip': tmp[2], 'nsp': tmp[0]}
-                    ports['iSCSI'].append(port)
+                    ports['iSCSI'].append(tmp[2])
 
         LOG.debug("PORTS = %s" % pprint.pformat(ports))
         return ports
@@ -438,9 +451,42 @@ exit
         client.deleteVLUN(volume_name, vlun['lun'], hostname)
         self._delete_3par_host(hostname)
 
+    def _get_volume_type(self, type_id):
+        ctxt = context.get_admin_context()
+        return volume_types.get_volume_type(ctxt, type_id)
+
+    def _get_volume_type_value(self, volume_type, key, default=None):
+        if volume_type is not None:
+            specs = volume_type.get('extra_specs')
+            if key in specs:
+                return specs[key]
+            else:
+                return default
+        else:
+            return default
+
+    def get_persona_type(self, volume):
+        default_persona = self.valid_persona_values[0]
+        type_id = volume.get('volume_type_id', None)
+        volume_type = None
+        if type_id is not None:
+            volume_type = self._get_volume_type(type_id)
+        persona_value = self._get_volume_type_value(volume_type, 'persona',
+                                                    default_persona)
+        if persona_value not in self.valid_persona_values:
+            err = _("Must specify a valid persona %(valid)s, "
+                    "value '%(persona)s' is invalid.") % \
+                   ({'valid': self.valid_persona_values,
+                     'persona': persona_value})
+            raise exception.InvalidInput(reason=err)
+        # persona is set by the id so remove the text and return the id
+        # i.e for persona '1 - Generic' returns 1
+        persona_id = persona_value.split(' ')
+        return persona_id[0]
+
     @lockutils.synchronized('3par', 'cinder-', True)
     def create_volume(self, volume, client, FLAGS):
-        """ Create a new volume """
+        """ Create a new volume. """
         LOG.debug("CREATE VOLUME (%s : %s %s)" %
                   (volume['display_name'], volume['name'],
                    self._get_3par_vol_name(volume['id'])))
@@ -453,16 +499,57 @@ exit
             if name:
                 comments['display_name'] = name
 
-            extras = {'comment': json.dumps(comments),
-                      'snapCPG': FLAGS.hp3par_cpg_snap}
+            # get the options supported by volume types
+            volume_type = None
+            type_id = volume.get('volume_type_id', None)
+            if type_id is not None:
+                volume_type = self._get_volume_type(type_id)
+
+            cpg = self._get_volume_type_value(volume_type, 'cpg',
+                                              FLAGS.hp3par_cpg)
+
+            # if provisioning is not set use thin
+            default_prov = self.valid_prov_values[0]
+            prov_value = self._get_volume_type_value(volume_type,
+                                                     'provisioning',
+                                                     default_prov)
+            # check for valid provisioning type
+            if prov_value not in self.valid_prov_values:
+                err = _("Must specify a valid provisioning type %(valid)s, "
+                        "value '%(prov)s' is invalid.") % \
+                       ({'valid': self.valid_prov_values,
+                         'prov': prov_value})
+                raise exception.InvalidInput(reason=err)
+
+            ttpv = True
+            if prov_value == "full":
+                ttpv = False
+
+            # default to hp3par_cpg if hp3par_cpg_snap is not set.
+            if FLAGS.hp3par_cpg_snap == "":
+                snap_default = FLAGS.hp3par_cpg
+            else:
+                snap_default = FLAGS.hp3par_cpg_snap
+            snap_cpg = self._get_volume_type_value(volume_type,
+                                                   'snap_cpg',
+                                                   snap_default)
 
-            if not FLAGS.hp3par_cpg_snap:
-                extras['snapCPG'] = FLAGS.hp3par_cpg
+            # check for valid persona even if we don't use it until
+            # attach time, this will given end user notice that the
+            # persona type is invalid at volume creation time
+            self.get_persona_type(volume)
+
+            if type_id is not None:
+                comments['volume_type_name'] = volume_type.get('name')
+                comments['volume_type_id'] = type_id
+
+            extras = {'comment': json.dumps(comments),
+                      'snapCPG': snap_cpg,
+                      'tpvv': ttpv}
 
             capacity = self._capacity_from_size(volume['size'])
             volume_name = self._get_3par_vol_name(volume['id'])
-            client.createVolume(volume_name, FLAGS.hp3par_cpg,
-                                capacity, extras)
+            client.createVolume(volume_name, cpg, capacity, extras)
 
         except hpexceptions.HTTPConflict:
             raise exception.Duplicate(_("Volume (%s) already exists on array")
@@ -470,6 +557,9 @@ exit
         except hpexceptions.HTTPBadRequest as ex:
             LOG.error(str(ex))
             raise exception.Invalid(ex.get_description())
+        except exception.InvalidInput as ex:
+            LOG.error(str(ex))
+            raise ex
         except Exception as ex:
             LOG.error(str(ex))
             raise exception.CinderException(ex.get_description())
@@ -478,9 +568,67 @@ exit
                     'snapCPG': extras['snapCPG']}
         return metadata
 
+    @lockutils.synchronized('3parcopy', 'cinder-', True)
+    def _copy_volume(self, src_name, dest_name):
+        self._cli_run('createvvcopy -p %s %s' % (src_name, dest_name), None)
+
+    @lockutils.synchronized('3parstate', 'cinder-', True)
+    def _get_volume_state(self, vol_name):
+        out = self._cli_run('showvv -state %s' % vol_name, None)
+        status = None
+        if out:
+            # out[0] is the header
+            info = out[1].split(',')
+            status = info[5]
+
+        return status
+
+    @lockutils.synchronized('3parclone', 'cinder-', True)
+    def create_cloned_volume(self, volume, src_vref, client, FLAGS):
+
+        try:
+            orig_name = self._get_3par_vol_name(volume['source_volid'])
+            vol_name = self._get_3par_vol_name(volume['id'])
+            # We need to create a new volume first.  Otherwise you
+            # can't delete the original
+            new_vol = self.create_volume(volume, client, FLAGS)
+
+            # make the 3PAR copy the contents.
+            # can't delete the original until the copy is done.
+            self._copy_volume(orig_name, vol_name)
+
+            # this can take a long time to complete
+            done = False
+            while not done:
+                status = self._get_volume_state(vol_name)
+                if status == 'normal':
+                    done = True
+                elif status == 'copy_target':
+                    LOG.debug("3Par still copying %s => %s"
+                              % (orig_name, vol_name))
+                else:
+                    msg = _("Unexpected state while cloning %s") % status
+                    LOG.warn(msg)
+                    raise exception.CinderException(msg)
+
+                if not done:
+                    # wait 5 seconds between tests
+                    time.sleep(5)
+
+            return new_vol
+        except hpexceptions.HTTPForbidden:
+            raise exception.NotAuthorized()
+        except hpexceptions.HTTPNotFound:
+            raise exception.NotFound()
+        except Exception as ex:
+            LOG.error(str(ex))
+            raise exception.CinderException(ex)
+
+        return None
+
     @lockutils.synchronized('3par', 'cinder-', True)
     def delete_volume(self, volume, client):
-        """ Delete a volume """
+        """ Delete a volume. """
         try:
             volume_name = self._get_3par_vol_name(volume['id'])
             client.deleteVolume(volume_name)
@@ -537,7 +685,7 @@ be the same as it's Snapshot."
 
     @lockutils.synchronized('3par', 'cinder-', True)
     def create_snapshot(self, snapshot, client, FLAGS):
-        """Creates a snapshot."""
+        """ Creates a snapshot. """
         LOG.debug("Create Snapshot\n%s" % pprint.pformat(snapshot))
 
         try:
@@ -575,7 +723,7 @@ be the same as it's Snapshot."
 
     @lockutils.synchronized('3par', 'cinder-', True)
     def delete_snapshot(self, snapshot, client):
-        """Driver entry point for deleting a snapshot."""
+        """ Driver entry point for deleting a snapshot. """
         LOG.debug("Delete Snapshot\n%s" % pprint.pformat(snapshot))
 
         try:
diff --git a/cinder/volume/drivers/san/hp/hp_3par_fc.py b/cinder/volume/drivers/san/hp/hp_3par_fc.py
new file mode 100644 (file)
index 0000000..d9d0334
--- /dev/null
@@ -0,0 +1,241 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+#
+#    (c) Copyright 2013 Hewlett-Packard Development Company, L.P.
+#    All Rights Reserved.
+#
+#    Copyright 2012 OpenStack LLC
+#
+#    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.
+#
+"""
+Volume driver for HP 3PAR Storage array. This driver requires 3.1.2 firmware
+on the 3PAR array. Set the following in the cinder.conf file to enable the
+3PAR Fibre Channel Driver along with the required flags:
+
+volume_driver=cinder.volume.drivers.san.hp.hp_3par_fc.HP3PARFCDriver
+"""
+
+from hp3parclient import client
+from hp3parclient import exceptions as hpexceptions
+
+from cinder import exception
+from cinder import flags
+from cinder.openstack.common import cfg
+from cinder.openstack.common import lockutils
+from cinder.openstack.common import log as logging
+import cinder.volume.driver
+from cinder.volume.drivers.san.hp.hp_3par_common import HP3PARCommon
+
+VERSION = 1.0
+LOG = logging.getLogger(__name__)
+
+FLAGS = flags.FLAGS
+
+
+class HP3PARFCDriver(cinder.volume.driver.FibreChannelDriver):
+    """OpenStack Fibre Channel driver to enable 3PAR storage array.
+
+    Version history:
+        1.0 - Initial driver
+
+    """
+
+    def __init__(self, *args, **kwargs):
+        super(HP3PARFCDriver, self).__init__(*args, **kwargs)
+        self.client = None
+        self.common = None
+
+    def _init_common(self):
+        return HP3PARCommon()
+
+    def _check_flags(self):
+        """Sanity check to ensure we have required options set."""
+        required_flags = ['hp3par_api_url', 'hp3par_username',
+                          'hp3par_password',
+                          'san_ip', 'san_login', 'san_password']
+        self.common.check_flags(FLAGS, required_flags)
+
+    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'] = 'FC'
+        stats['volume_backend_name'] = 'HP3PARFCDriver'
+        return stats
+
+    def do_setup(self, context):
+        self.common = self._init_common()
+        self._check_flags()
+        self.client = self._create_client()
+        if FLAGS.hp3par_debug:
+            self.client.debug_rest(True)
+
+        try:
+            LOG.debug("Connecting to 3PAR")
+            self.client.login(FLAGS.hp3par_username, FLAGS.hp3par_password)
+        except hpexceptions.HTTPUnauthorized as ex:
+            LOG.warning("Failed to connect to 3PAR (%s) because %s" %
+                       (FLAGS.hp3par_api_url, str(ex)))
+            msg = _("Login to 3PAR array invalid")
+            raise exception.InvalidInput(reason=msg)
+
+        # make sure the CPG exists
+        try:
+            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)
+
+    def check_for_setup_error(self):
+        """Returns an error if prerequisites aren't met."""
+        self._check_flags()
+
+    def create_volume(self, volume):
+        """ Create a new volume. """
+        metadata = self.common.create_volume(volume, self.client, FLAGS)
+        return {'metadata': metadata}
+
+    def create_cloned_volume(self, volume, src_vref):
+        """ Clone an existing volume. """
+        new_vol = self.common.create_cloned_volume(volume, src_vref,
+                                                   self.client, FLAGS)
+        return {'metadata': new_vol}
+
+    def delete_volume(self, volume):
+        """ Delete a volume. """
+        self.common.delete_volume(volume, self.client)
+
+    def create_volume_from_snapshot(self, volume, snapshot):
+        """
+        Creates a volume from a snapshot.
+
+        TODO: support using the size from the user.
+        """
+        self.common.create_volume_from_snapshot(volume, snapshot, self.client)
+
+    def create_snapshot(self, snapshot):
+        """Creates a snapshot."""
+        self.common.create_snapshot(snapshot, self.client, FLAGS)
+
+    def delete_snapshot(self, snapshot):
+        """Driver entry point for deleting a snapshot."""
+        self.common.delete_snapshot(snapshot, self.client)
+
+    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.
+
+        The  driver returns a driver_volume_type of 'fibre_channel'.
+        The target_wwn can be a single entry or a list of wwns that
+        correspond to the list of remote wwn(s) that will export the volume.
+        Example return values:
+
+            {
+                'driver_volume_type': 'fibre_channel'
+                'data': {
+                    'target_discovered': True,
+                    'target_lun': 1,
+                    'target_wwn': '1234567890123',
+                }
+            }
+
+            or
+
+             {
+                'driver_volume_type': 'fibre_channel'
+                'data': {
+                    'target_discovered': True,
+                    'target_lun': 1,
+                    'target_wwn': ['1234567890123', '0987654321321'],
+                }
+            }
+
+
+        Steps to export a volume on 3PAR
+          * Create a host on the 3par with the target wwn
+          * Create a VLUN for that HOST with the volume we want to export.
+
+        """
+        # we have to make sure we have a host
+        host = self._create_host(volume, connector)
+
+        # now that we have a host, create the VLUN
+        vlun = self.common.create_vlun(volume, host, self.client)
+
+        ports = self.common.get_ports()
+
+        info = {'driver_volume_type': 'fibre_channel',
+                'data': {'target_lun': vlun['lun'],
+                         'target_discovered': True,
+                         'target_wwn': ports['FC']}}
+        return info
+
+    def terminate_connection(self, volume, connector, force):
+        """
+        Driver entry point to unattach a volume from an instance.
+        """
+        self.common.delete_vlun(volume, connector, self.client)
+        pass
+
+    def _create_3par_fibrechan_host(self, hostname, wwn, domain, persona_id):
+        out = self.common._cli_run('createhost -persona %s -domain %s %s %s'
+                                   % (persona_id, domain,
+                                      hostname, " ".join(wwn)), None)
+
+    def _modify_3par_fibrechan_host(self, hostname, wwn):
+        # when using -add, you can not send the persona or domain options
+        out = self.common._cli_run('createhost -add %s %s'
+                                   % (hostname, " ".join(wwn)), None)
+
+    def _create_host(self, volume, connector):
+        """
+        This is a 3PAR host entry for exporting volumes
+        via active VLUNs.
+        """
+        host = None
+        hostname = self.common._safe_hostname(connector['host'])
+        try:
+            host = self.common._get_3par_host(hostname)
+            if not host['FCPaths']:
+                self._modify_3par_fibrechan_host(hostname, connector['wwpns'])
+                host = self.common._get_3par_host(hostname)
+        except hpexceptions.HTTPNotFound as ex:
+            # get persona from the volume type extra specs
+            persona_id = self.common.get_persona_type(volume)
+            # host doesn't exist, we have to create it
+            self._create_3par_fibrechan_host(hostname, connector['wwpns'],
+                                             FLAGS.hp3par_domain, persona_id)
+            host = self.common._get_3par_host(hostname)
+
+        return host
+
+    def create_export(self, context, volume):
+        pass
+
+    def ensure_export(self, context, volume):
+        """Exports the volume."""
+        pass
+
+    def remove_export(self, context, volume):
+        """Removes an export for a logical volume."""
+        pass
index 9548b4f4e25fcc2f9a11283776e0fd714a66ff7b..5b76900749378c78bcb450f41062de91b1fd18ae 100644 (file)
@@ -1,6 +1,6 @@
 # vim: tabstop=4 shiftwidth=4 softtabstop=4
 #
-#    Copyright (c) 2012 Hewlett-Packard, Inc.
+#    (c) Copyright 2012-2013 Hewlett-Packard Development Company, L.P.
 #    All Rights Reserved.
 #
 #    Copyright 2012 OpenStack LLC
 #    under the License.
 #
 """
-Volume driver for HP 3PAR Storage array
-This driver requires 3.1.2 firmware on
-the 3Par array
+Volume driver for HP 3PAR Storage array. This driver requires 3.1.2 firmware
+on the 3PAR array. Set the following in the cinder.conf file to enable the
+3PAR iSCSI Driver along with the required flags:
+
+volume_driver=cinder.volume.drivers.san.hp.hp_3par_iscsi.HP3PARISCSIDriver
 """
 
 from hp3parclient import client
@@ -33,13 +35,19 @@ from cinder.openstack.common import log as logging
 import cinder.volume.driver
 from cinder.volume.drivers.san.hp.hp_3par_common import HP3PARCommon
 
+VERSION = 1.0
 LOG = logging.getLogger(__name__)
 
 FLAGS = flags.FLAGS
 
 
 class HP3PARISCSIDriver(cinder.volume.driver.ISCSIDriver):
+    """OpenStack iSCSI driver to enable 3PAR storage array.
+
+    Version history:
+        1.0 - Initial driver
 
+    """
     def __init__(self, *args, **kwargs):
         super(HP3PARISCSIDriver, self).__init__(*args, **kwargs)
         self.client = None
@@ -104,16 +112,24 @@ must be the same" % (cpg['domain'], FLAGS.hp3par_domain)
 
     @lockutils.synchronized('3par-vol', 'cinder-', True)
     def create_volume(self, volume):
-        """ Create a new volume """
+        """ Create a new volume. """
         metadata = self.common.create_volume(volume, self.client, FLAGS)
 
         return {'provider_location': "%s:%s" %
                 (FLAGS.iscsi_ip_address, FLAGS.iscsi_port),
                 'metadata': metadata}
 
+    def create_cloned_volume(self, volume, src_vref):
+        """ Clone an existing volume. """
+        new_vol = self.common.create_cloned_volume(volume, src_vref,
+                                                   self.client, FLAGS)
+        return {'provider_location': "%s:%s" %
+                (FLAGS.iscsi_ip_address, FLAGS.iscsi_port),
+                'metadata': new_vol}
+
     @lockutils.synchronized('3par-vol', 'cinder-', True)
     def delete_volume(self, volume):
-        """ Delete a volume """
+        """ Delete a volume. """
         self.common.delete_volume(volume, self.client)
 
     @lockutils.synchronized('3par-vol', 'cinder-', True)
@@ -202,9 +218,9 @@ must be the same" % (cpg['domain'], FLAGS.hp3par_domain)
 
         return iqn
 
-    def _create_3par_iscsi_host(self, hostname, iscsi_iqn, domain):
-        cmd = 'createhost -iscsi -persona 1 -domain %s %s %s' % \
-              (domain, hostname, iscsi_iqn)
+    def _create_3par_iscsi_host(self, hostname, iscsi_iqn, domain, persona_id):
+        cmd = 'createhost -iscsi -persona %s -domain %s %s %s' % \
+              (persona_id, domain, hostname, iscsi_iqn)
         self.common._cli_run(cmd, None)
 
     def _modify_3par_iscsi_host(self, hostname, iscsi_iqn):
@@ -215,7 +231,7 @@ must be the same" % (cpg['domain'], FLAGS.hp3par_domain)
     def _create_host(self, volume, connector):
         """
         This is a 3PAR host entry for exporting volumes
-        via active VLUNs
+        via active VLUNs.
         """
         # make sure we don't have the host already
         host = None
@@ -226,9 +242,11 @@ must be the same" % (cpg['domain'], FLAGS.hp3par_domain)
                 self._modify_3par_iscsi_host(hostname, connector['initiator'])
                 host = self.common._get_3par_host(hostname)
         except hpexceptions.HTTPNotFound:
+            # get persona from the volume type extra specs
+            persona_id = self.common.get_persona_type(volume)
             # host doesn't exist, we have to create it
             self._create_3par_iscsi_host(hostname, connector['initiator'],
-                                         FLAGS.hp3par_domain)
+                                         FLAGS.hp3par_domain, persona_id)
             host = self.common._get_3par_host(hostname)
 
         return host