]> review.fuel-infra Code Review - openstack-build/cinder-build.git/commitdiff
Add CHAP support for 3PAR ISCSI
authorAnthony Lee <anthony.mic.lee@hp.com>
Tue, 22 Jul 2014 18:40:21 +0000 (11:40 -0700)
committerAnthony Lee <anthony.mic.lee@hp.com>
Fri, 8 Aug 2014 18:46:35 +0000 (11:46 -0700)
Adds CHAP support to 3PAR ISCSI.  Volume metadata will store
CHAP credentials to allow volumes to be attached to hosts on
a 3PAR backend.  Credentials are generated when the first volume
is attached.  Subsequent volumes lookup the CHAP credentials
from an existing volume already attached to the host.

Implements: blueprint add-chap-support-3par-iscsi
DocImpact

Change-Id: I45d77c4243339e8fc76969042634c6c45899fbae

cinder/tests/fake_hp_3par_client.py
cinder/tests/test_hp3par.py
cinder/volume/drivers/san/hp/hp_3par_common.py
cinder/volume/drivers/san/hp/hp_3par_iscsi.py
etc/cinder/cinder.conf.sample

index 2834c0150890e7c24de7af3c1db1119df12b326e..073eb2f0018399e0ecf777ca5fb70a946166cebf 100644 (file)
@@ -21,7 +21,7 @@ import sys
 from cinder.tests import fake_hp_client_exceptions as hpexceptions
 
 hp3par = mock.Mock()
-hp3par.version = "3.0.0"
+hp3par.version = "3.1.0"
 hp3par.exceptions = hpexceptions
 
 sys.modules['hp3parclient'] = hp3par
index b423522b34f4c74bba9018472351e1df4392dd94..2533b60c4e12052a441ecc46220fd0da8cf81b2b 100644 (file)
@@ -42,6 +42,9 @@ HP3PAR_SAN_SSH_PORT = 999
 HP3PAR_SAN_SSH_CON_TIMEOUT = 44
 HP3PAR_SAN_SSH_PRIVATE = 'foobar'
 
+CHAP_USER_KEY = "HPQ-cinder-CHAP-name"
+CHAP_PASS_KEY = "HPQ-cinder-CHAP-secret"
+
 
 class HP3PARBaseDriver(object):
 
@@ -171,6 +174,8 @@ class HP3PARBaseDriver(object):
         'TASK_DONE': TASK_DONE,
         'TASK_ACTIVE': TASK_ACTIVE,
         'HOST_EDIT_ADD': 1,
+        'CHAP_INITIATOR': 1,
+        'CHAP_TARGET': 2,
         'getPorts.return_value': {
             'members': FAKE_FC_PORTS + [FAKE_ISCSI_PORT]
         }
@@ -338,6 +343,7 @@ class HP3PARBaseDriver(object):
         configuration.hp3par_snapshot_expiration = ""
         configuration.hp3par_snapshot_retention = ""
         configuration.hp3par_iscsi_ips = []
+        configuration.hp3par_iscsi_chap_enabled = False
         return configuration
 
     @mock.patch(
@@ -1204,6 +1210,10 @@ class HP3PARBaseDriver(object):
                 None,
                 self.FAKE_HOST),
             mock.call.deleteHost(self.FAKE_HOST),
+            mock.call.removeVolumeMetaData(
+                self.VOLUME_3PAR_NAME, CHAP_USER_KEY),
+            mock.call.removeVolumeMetaData(
+                self.VOLUME_3PAR_NAME, CHAP_PASS_KEY),
             mock.call.logout()]
 
         mock_client.assert_has_calls(expected)
@@ -2221,7 +2231,8 @@ class TestHP3PARISCSIDriver(HP3PARBaseDriver, test.TestCase):
         mock_client.findHost.return_value = None
         mock_client.getVLUN.return_value = {'lun': self.TARGET_LUN}
 
-        host = self.driver._create_host(self.volume, self.connector)
+        host, auth_username, auth_password = self.driver._create_host(
+            self.volume, self.connector)
         expected = [
             mock.call.getVolume('osv-0DM4qZEVSKON-DXN-NwVpw'),
             mock.call.getCPG(HP3PAR_CPG),
@@ -2236,6 +2247,67 @@ class TestHP3PARISCSIDriver(HP3PARBaseDriver, test.TestCase):
         mock_client.assert_has_calls(expected)
 
         self.assertEqual(host['name'], self.FAKE_HOST)
+        self.assertEqual(auth_username, None)
+        self.assertEqual(auth_password, None)
+
+    def test_create_host_chap_enabled(self):
+        # setup_mock_client drive with CHAP enabled configuration
+        # and return the mock HTTP 3PAR client
+        config = self.setup_configuration()
+        config.hp3par_iscsi_chap_enabled = True
+        mock_client = self.setup_driver(config=config)
+
+        mock_client.getVolume.return_value = {'userCPG': HP3PAR_CPG}
+        mock_client.getCPG.return_value = {}
+        mock_client.getHost.side_effect = [
+            hpexceptions.HTTPNotFound('fake'),
+            {'name': self.FAKE_HOST}]
+        mock_client.findHost.return_value = None
+        mock_client.getVLUN.return_value = {'lun': self.TARGET_LUN}
+
+        expected_mod_request = {
+            'chapOperation': mock_client.HOST_EDIT_ADD,
+            'chapOperationMode': mock_client.CHAP_INITIATOR,
+            'chapName': 'test-user',
+            'chapSecret': 'test-pass'
+        }
+
+        def get_side_effect(*args):
+            data = {'value': None}
+            if args[1] == CHAP_USER_KEY:
+                data['value'] = 'test-user'
+            elif args[1] == CHAP_PASS_KEY:
+                data['value'] = 'test-pass'
+            return data
+
+        mock_client.getVolumeMetaData.side_effect = get_side_effect
+
+        host, auth_username, auth_password = self.driver._create_host(
+            self.volume, self.connector)
+        expected = [
+            mock.call.getVolume('osv-0DM4qZEVSKON-DXN-NwVpw'),
+            mock.call.getCPG(HP3PAR_CPG),
+            mock.call.getVolumeMetaData(
+                'osv-0DM4qZEVSKON-DXN-NwVpw', CHAP_USER_KEY),
+            mock.call.getVolumeMetaData(
+                'osv-0DM4qZEVSKON-DXN-NwVpw', CHAP_PASS_KEY),
+            mock.call.getHost(self.FAKE_HOST),
+            mock.call.findHost(iqn='iqn.1993-08.org.debian:01:222'),
+            mock.call.createHost(
+                self.FAKE_HOST,
+                optional={'domain': None, 'persona': 1},
+                iscsiNames=['iqn.1993-08.org.debian:01:222']),
+            mock.call.modifyHost(
+                'fakehost',
+                expected_mod_request),
+            mock.call.getHost(self.FAKE_HOST)
+        ]
+
+        mock_client.assert_has_calls(expected)
+
+        self.assertEqual(host['name'], self.FAKE_HOST)
+        self.assertEqual(auth_username, 'test-user')
+        self.assertEqual(auth_password, 'test-pass')
 
     def test_create_invalid_host(self):
         # setup_mock_client drive with default configuration
@@ -2248,7 +2320,8 @@ class TestHP3PARISCSIDriver(HP3PARBaseDriver, test.TestCase):
             {'name': 'fakehost.foo'}]
         mock_client.findHost.return_value = 'fakehost.foo'
 
-        host = self.driver._create_host(self.volume, self.connector)
+        host, auth_username, auth_password = self.driver._create_host(
+            self.volume, self.connector)
 
         expected = [
             mock.call.getVolume('osv-0DM4qZEVSKON-DXN-NwVpw'),
@@ -2260,6 +2333,63 @@ class TestHP3PARISCSIDriver(HP3PARBaseDriver, test.TestCase):
         mock_client.assert_has_calls(expected)
 
         self.assertEqual(host['name'], 'fakehost.foo')
+        self.assertEqual(auth_username, None)
+        self.assertEqual(auth_password, None)
+
+    def test_create_invalid_host_chap_enabled(self):
+        # setup_mock_client drive with CHAP enabled configuration
+        # and return the mock HTTP 3PAR client
+        config = self.setup_configuration()
+        config.hp3par_iscsi_chap_enabled = True
+        mock_client = self.setup_driver(config=config)
+
+        mock_client.getVolume.return_value = {'userCPG': HP3PAR_CPG}
+        mock_client.getCPG.return_value = {}
+        mock_client.getHost.side_effect = [
+            hpexceptions.HTTPNotFound('Host not found.'),
+            {'name': 'fakehost.foo'}]
+        mock_client.findHost.return_value = 'fakehost.foo'
+
+        def get_side_effect(*args):
+            data = {'value': None}
+            if args[1] == CHAP_USER_KEY:
+                data['value'] = 'test-user'
+            elif args[1] == CHAP_PASS_KEY:
+                data['value'] = 'test-pass'
+            return data
+
+        mock_client.getVolumeMetaData.side_effect = get_side_effect
+
+        expected_mod_request = {
+            'chapOperation': mock_client.HOST_EDIT_ADD,
+            'chapOperationMode': mock_client.CHAP_INITIATOR,
+            'chapName': 'test-user',
+            'chapSecret': 'test-pass'
+        }
+
+        host, auth_username, auth_password = self.driver._create_host(
+            self.volume, self.connector)
+
+        expected = [
+            mock.call.getVolume('osv-0DM4qZEVSKON-DXN-NwVpw'),
+            mock.call.getCPG(HP3PAR_CPG),
+            mock.call.getVolumeMetaData(
+                'osv-0DM4qZEVSKON-DXN-NwVpw', CHAP_USER_KEY),
+            mock.call.getVolumeMetaData(
+                'osv-0DM4qZEVSKON-DXN-NwVpw', CHAP_PASS_KEY),
+            mock.call.getHost(self.FAKE_HOST),
+            mock.call.findHost(iqn='iqn.1993-08.org.debian:01:222'),
+            mock.call.modifyHost(
+                'fakehost.foo',
+                expected_mod_request),
+            mock.call.getHost('fakehost.foo')
+        ]
+
+        mock_client.assert_has_calls(expected)
+
+        self.assertEqual(host['name'], 'fakehost.foo')
+        self.assertEqual(auth_username, 'test-user')
+        self.assertEqual(auth_password, 'test-pass')
 
     def test_create_modify_host(self):
         # setup_mock_client drive with default configuration
@@ -2273,7 +2403,8 @@ class TestHP3PARISCSIDriver(HP3PARBaseDriver, test.TestCase):
              'FCPaths': [{'wwn': '123456789012345'},
                          {'wwn': '123456789054321'}]}]
 
-        host = self.driver._create_host(self.volume, self.connector)
+        host, auth_username, auth_password = self.driver._create_host(
+            self.volume, self.connector)
 
         expected = [
             mock.call.getVolume('osv-0DM4qZEVSKON-DXN-NwVpw'),
@@ -2288,6 +2419,68 @@ class TestHP3PARISCSIDriver(HP3PARBaseDriver, test.TestCase):
         mock_client.assert_has_calls(expected)
 
         self.assertEqual(host['name'], self.FAKE_HOST)
+        self.assertEqual(auth_username, None)
+        self.assertEqual(auth_password, None)
+        self.assertEqual(len(host['FCPaths']), 2)
+
+    def test_create_modify_host_chap_enabled(self):
+        # setup_mock_client drive with CHAP enabled configuration
+        # and return the mock HTTP 3PAR client
+        config = self.setup_configuration()
+        config.hp3par_iscsi_chap_enabled = True
+        mock_client = self.setup_driver(config=config)
+
+        mock_client.getVolume.return_value = {'userCPG': HP3PAR_CPG}
+        mock_client.getCPG.return_value = {}
+        mock_client.getHost.side_effect = [
+            {'name': self.FAKE_HOST, 'FCPaths': []},
+            {'name': self.FAKE_HOST,
+             'FCPaths': [{'wwn': '123456789012345'},
+                         {'wwn': '123456789054321'}]}]
+
+        def get_side_effect(*args):
+            data = {'value': None}
+            if args[1] == CHAP_USER_KEY:
+                data['value'] = 'test-user'
+            elif args[1] == CHAP_PASS_KEY:
+                data['value'] = 'test-pass'
+            return data
+
+        mock_client.getVolumeMetaData.side_effect = get_side_effect
+
+        expected_mod_request = {
+            'chapOperation': mock_client.HOST_EDIT_ADD,
+            'chapOperationMode': mock_client.CHAP_INITIATOR,
+            'chapName': 'test-user',
+            'chapSecret': 'test-pass'
+        }
+
+        host, auth_username, auth_password = self.driver._create_host(
+            self.volume, self.connector)
+
+        expected = [
+            mock.call.getVolume('osv-0DM4qZEVSKON-DXN-NwVpw'),
+            mock.call.getCPG(HP3PAR_CPG),
+            mock.call.getVolumeMetaData(
+                'osv-0DM4qZEVSKON-DXN-NwVpw', CHAP_USER_KEY),
+            mock.call.getVolumeMetaData(
+                'osv-0DM4qZEVSKON-DXN-NwVpw', CHAP_PASS_KEY),
+            mock.call.getHost(self.FAKE_HOST),
+            mock.call.modifyHost(
+                self.FAKE_HOST,
+                {'pathOperation': 1,
+                    'iSCSINames': ['iqn.1993-08.org.debian:01:222']}),
+            mock.call.modifyHost(
+                self.FAKE_HOST,
+                expected_mod_request
+            ),
+            mock.call.getHost(self.FAKE_HOST)]
+
+        mock_client.assert_has_calls(expected)
+
+        self.assertEqual(host['name'], self.FAKE_HOST)
+        self.assertEqual(auth_username, 'test-user')
+        self.assertEqual(auth_password, 'test-pass')
         self.assertEqual(len(host['FCPaths']), 2)
 
     def test_get_least_used_nsp_for_host_single(self):
@@ -2457,6 +2650,310 @@ class TestHP3PARISCSIDriver(HP3PARBaseDriver, test.TestCase):
                                               ['1:1:1', '1:2:1'])
         self.assertEqual(nsp, '1:1:1')
 
+    def test_set_3par_chaps(self):
+        # setup_mock_client drive with default configuration
+        # and return the mock HTTP 3PAR client
+        mock_client = self.setup_driver()
+
+        expected = []
+        self.driver._set_3par_chaps(
+            'test-host', 'test-vol', 'test-host', 'pass')
+        mock_client.assert_has_calls(expected)
+
+        # setup_mock_client drive with CHAP enabled configuration
+        # and return the mock HTTP 3PAR client
+        config = self.setup_configuration()
+        config.hp3par_iscsi_chap_enabled = True
+        mock_client = self.setup_driver(config=config)
+
+        expected_mod_request = {
+            'chapOperation': mock_client.HOST_EDIT_ADD,
+            'chapOperationMode': mock_client.CHAP_INITIATOR,
+            'chapName': 'test-host',
+            'chapSecret': 'fake'
+        }
+
+        expected = [
+            mock.call.modifyHost('test-host', expected_mod_request)
+        ]
+        self.driver._set_3par_chaps(
+            'test-host', 'test-vol', 'test-host', 'fake')
+        mock_client.assert_has_calls(expected)
+
+    @mock.patch('cinder.volume.utils.generate_password')
+    def test_do_export(self, mock_utils):
+        # setup_mock_client drive with default configuration
+        # and return the mock HTTP 3PAR client
+        mock_client = self.setup_driver()
+
+        volume = {'host': 'test-host@3pariscsi',
+                  'id': 'd03338a9-9115-48a3-8dfc-35cdfcdc15a7'}
+        mock_utils.return_value = 'random-pass'
+        mock_client.getHostVLUNs.return_value = [
+            {'active': True,
+             'volumeName': self.VOLUME_3PAR_NAME,
+             'lun': None, 'type': 0,
+             'remoteName': 'iqn.1993-08.org.debian:01:222'}
+        ]
+        mock_client.getHost.return_value = {
+            'name': 'osv-0DM4qZEVSKON-DXN-NwVpw',
+            'initiatorChapEnabled': True
+        }
+        mock_client.getVolumeMetaData.return_value = {
+            'value': 'random-pass'
+        }
+
+        expected = []
+        expected_model = {'provider_auth': None}
+        model = self.driver._do_export(volume)
+
+        mock_client.assert_has_calls(expected)
+        self.assertEqual(expected_model, model)
+
+        mock_client.reset_mock()
+
+        # setup_mock_client drive with CHAP enabled configuration
+        # and return the mock HTTP 3PAR client
+        config = self.setup_configuration()
+        config.hp3par_iscsi_chap_enabled = True
+        mock_client = self.setup_driver(config=config)
+
+        volume = {'host': 'test-host@3pariscsi',
+                  'id': 'd03338a9-9115-48a3-8dfc-35cdfcdc15a7'}
+        mock_utils.return_value = 'random-pass'
+        mock_client.getHostVLUNs.return_value = [
+            {'active': True,
+             'volumeName': self.VOLUME_3PAR_NAME,
+             'lun': None, 'type': 0,
+             'remoteName': 'iqn.1993-08.org.debian:01:222'}
+        ]
+        mock_client.getHost.return_value = {
+            'name': 'osv-0DM4qZEVSKON-DXN-NwVpw',
+            'initiatorChapEnabled': True
+        }
+        mock_client.getVolumeMetaData.return_value = {
+            'value': 'random-pass'
+        }
+
+        expected = [
+            mock.call.getHostVLUNs('test-host'),
+            mock.call.getHost('test-host'),
+            mock.call.getVolumeMetaData(
+                'osv-0DM4qZEVSKON-DXN-NwVpw', CHAP_PASS_KEY),
+            mock.call.setVolumeMetaData(
+                'osv-0DM4qZEVSKON-DXN-NwVpw', CHAP_USER_KEY, 'test-host'),
+            mock.call.setVolumeMetaData(
+                'osv-0DM4qZEVSKON-DXN-NwVpw', CHAP_PASS_KEY, 'random-pass')
+        ]
+        expected_model = {'provider_auth': 'CHAP test-host random-pass'}
+
+        model = self.driver._do_export(volume)
+        mock_client.assert_has_calls(expected)
+        self.assertEqual(expected_model, model)
+
+    @mock.patch('cinder.volume.utils.generate_password')
+    def test_do_export_host_not_found(self, mock_utils):
+        # setup_mock_client drive with CHAP enabled configuration
+        # and return the mock HTTP 3PAR client
+        config = self.setup_configuration()
+        config.hp3par_iscsi_chap_enabled = True
+        mock_client = self.setup_driver(config=config)
+
+        volume = {'host': 'test-host@3pariscsi',
+                  'id': 'd03338a9-9115-48a3-8dfc-35cdfcdc15a7'}
+        mock_utils.return_value = "random-pass"
+        mock_client.getHostVLUNs.side_effect = hpexceptions.HTTPNotFound(
+            'fake')
+
+        mock_client.getVolumeMetaData.return_value = {
+            'value': 'random-pass'
+        }
+
+        config = self.setup_configuration()
+        config.hp3par_iscsi_chap_enabled = True
+
+        expected = [
+            mock.call.getHostVLUNs('test-host'),
+            mock.call.setVolumeMetaData(
+                'osv-0DM4qZEVSKON-DXN-NwVpw', CHAP_USER_KEY, 'test-host'),
+            mock.call.setVolumeMetaData(
+                'osv-0DM4qZEVSKON-DXN-NwVpw', CHAP_PASS_KEY, 'random-pass')
+        ]
+        expected_model = {'provider_auth': 'CHAP test-host random-pass'}
+
+        model = self.driver._do_export(volume)
+        mock_client.assert_has_calls(expected)
+        self.assertEqual(expected_model, model)
+
+    @mock.patch('cinder.volume.utils.generate_password')
+    def test_do_export_host_chap_disabled(self, mock_utils):
+        # setup_mock_client drive with CHAP enabled configuration
+        # and return the mock HTTP 3PAR client
+        config = self.setup_configuration()
+        config.hp3par_iscsi_chap_enabled = True
+        mock_client = self.setup_driver(config=config)
+
+        volume = {'host': 'test-host@3pariscsi',
+                  'id': 'd03338a9-9115-48a3-8dfc-35cdfcdc15a7'}
+        mock_utils.return_value = 'random-pass'
+        mock_client.getHostVLUNs.return_value = [
+            {'active': True,
+             'volumeName': self.VOLUME_3PAR_NAME,
+             'lun': None, 'type': 0,
+             'remoteName': 'iqn.1993-08.org.debian:01:222'}
+        ]
+        mock_client.getHost.return_value = {
+            'name': 'fake-host',
+            'initiatorChapEnabled': False
+        }
+        mock_client.getVolumeMetaData.return_value = {
+            'value': 'random-pass'
+        }
+
+        mock_client.reset_mock()
+
+        config = self.setup_configuration()
+        config.hp3par_iscsi_chap_enabled = True
+
+        expected = [
+            mock.call.getHostVLUNs('test-host'),
+            mock.call.getHost('test-host'),
+            mock.call.getVolumeMetaData(
+                'osv-0DM4qZEVSKON-DXN-NwVpw', CHAP_PASS_KEY),
+            mock.call.setVolumeMetaData(
+                'osv-0DM4qZEVSKON-DXN-NwVpw', CHAP_USER_KEY, 'test-host'),
+            mock.call.setVolumeMetaData(
+                'osv-0DM4qZEVSKON-DXN-NwVpw', CHAP_PASS_KEY, 'random-pass')
+        ]
+        expected_model = {'provider_auth': 'CHAP test-host random-pass'}
+
+        model = self.driver._do_export(volume)
+        mock_client.assert_has_calls(expected)
+        self.assertEqual(expected_model, model)
+
+    @mock.patch('cinder.volume.utils.generate_password')
+    def test_do_export_no_active_vluns(self, mock_utils):
+        # setup_mock_client drive with CHAP enabled configuration
+        # and return the mock HTTP 3PAR client
+        config = self.setup_configuration()
+        config.hp3par_iscsi_chap_enabled = True
+        mock_client = self.setup_driver(config=config)
+
+        volume = {'host': 'test-host@3pariscsi',
+                  'id': 'd03338a9-9115-48a3-8dfc-35cdfcdc15a7'}
+        mock_utils.return_value = "random-pass"
+        mock_client.getHostVLUNs.return_value = [
+            {'active': False,
+             'volumeName': self.VOLUME_3PAR_NAME,
+             'lun': None, 'type': 0,
+             'remoteName': 'iqn.1993-08.org.debian:01:222'}
+        ]
+        mock_client.getHost.return_value = {
+            'name': 'fake-host',
+            'initiatorChapEnabled': True
+        }
+        mock_client.getVolumeMetaData.return_value = {
+            'value': 'random-pass'
+        }
+
+        expected = [
+            mock.call.getHostVLUNs('test-host'),
+            mock.call.getHost('test-host'),
+            mock.call.setVolumeMetaData(
+                'osv-0DM4qZEVSKON-DXN-NwVpw', CHAP_USER_KEY, 'test-host'),
+            mock.call.setVolumeMetaData(
+                'osv-0DM4qZEVSKON-DXN-NwVpw', CHAP_PASS_KEY, 'random-pass')
+        ]
+        expected_model = {'provider_auth': 'CHAP test-host random-pass'}
+
+        model = self.driver._do_export(volume)
+        mock_client.assert_has_calls(expected)
+        self.assertEqual(model, expected_model)
+
+    def test_ensure_export(self):
+        # setup_mock_client drive with default configuration
+        # and return the mock HTTP 3PAR client
+        mock_client = self.setup_driver()
+
+        volume = {'host': 'test-host@3pariscsi',
+                  'id': 'd03338a9-9115-48a3-8dfc-35cdfcdc15a7'}
+
+        mock_client.getAllVolumeMetaData.return_value = {
+            'total': 0,
+            'members': []
+        }
+
+        model = self.driver.ensure_export(None, volume)
+
+        expected = [
+            mock.call.login(HP3PAR_USER_NAME, HP3PAR_USER_PASS),
+            mock.call.getVolume('osv-0DM4qZEVSKON-DXN-NwVpw'),
+            mock.call.getAllVolumeMetaData('osv-0DM4qZEVSKON-DXN-NwVpw'),
+            mock.call.logout()
+        ]
+
+        expected_model = {'provider_auth': None}
+
+        mock_client.assert_has_calls(expected)
+        self.assertEqual(model, expected_model)
+
+        mock_client.getAllVolumeMetaData.return_value = {
+            'total': 2,
+            'members': [
+                {
+                    'creationTimeSec': 1406074222,
+                    'value': 'fake-host',
+                    'key': CHAP_USER_KEY,
+                    'creationTime8601': '2014-07-22T17:10:22-07:00'
+                },
+                {
+                    'creationTimeSec': 1406074222,
+                    'value': 'random-pass',
+                    'key': CHAP_PASS_KEY,
+                    'creationTime8601': '2014-07-22T17:10:22-07:00'
+                }
+            ]
+        }
+
+        model = self.driver.ensure_export(None, volume)
+
+        expected = [
+            mock.call.login(HP3PAR_USER_NAME, HP3PAR_USER_PASS),
+            mock.call.getVolume('osv-0DM4qZEVSKON-DXN-NwVpw'),
+            mock.call.getAllVolumeMetaData('osv-0DM4qZEVSKON-DXN-NwVpw'),
+            mock.call.logout()
+        ]
+
+        expected_model = {'provider_auth': "CHAP fake-host random-pass"}
+
+        mock_client.assert_has_calls(expected)
+        self.assertEqual(model, expected_model)
+
+    def test_ensure_export_missing_volume(self):
+        # setup_mock_client drive with default configuration
+        # and return the mock HTTP 3PAR client
+        mock_client = self.setup_driver()
+
+        volume = {'host': 'test-host@3pariscsi',
+                  'id': 'd03338a9-9115-48a3-8dfc-35cdfcdc15a7'}
+
+        mock_client.getVolume.side_effect = hpexceptions.HTTPNotFound(
+            'fake')
+
+        model = self.driver.ensure_export(None, volume)
+
+        expected = [
+            mock.call.login(HP3PAR_USER_NAME, HP3PAR_USER_PASS),
+            mock.call.getVolume('osv-0DM4qZEVSKON-DXN-NwVpw'),
+            mock.call.logout()
+        ]
+
+        expected_model = None
+
+        mock_client.assert_has_calls(expected)
+        self.assertEqual(model, expected_model)
+
+
 VLUNS5_RET = ({'members':
                [{'portPos': {'node': 0, 'slot': 8, 'cardPort': 2},
                  'active': True},
index 6f3931464c7543fac8f7d13730f2ed075842a31e..820c9eb27c47e1054208d2579797fa04f88a7945 100644 (file)
@@ -66,7 +66,7 @@ from taskflow.patterns import linear_flow
 
 LOG = logging.getLogger(__name__)
 
-MIN_CLIENT_VERSION = '3.0.0'
+MIN_CLIENT_VERSION = '3.1.0'
 
 hp3par_opts = [
     cfg.StrOpt('hp3par_api_url',
@@ -100,7 +100,10 @@ hp3par_opts = [
                 help="Enable HTTP debugging to 3PAR"),
     cfg.ListOpt('hp3par_iscsi_ips',
                 default=[],
-                help="List of target iSCSI addresses to use.")
+                help="List of target iSCSI addresses to use."),
+    cfg.BoolOpt('hp3par_iscsi_chap_enabled',
+                default=False,
+                help="Enable CHAP authentication for iSCSI connections."),
 ]
 
 
@@ -138,10 +141,13 @@ class HP3PARCommon(object):
         2.0.14 - Modified manage volume to use standard 'source-name' element.
         2.0.15 - Added support for volume retype
         2.0.16 - Add a better log during delete_volume time. Bug #1349636
+        2.0.17 - Added iSCSI CHAP support
+                 This update now requires 3.1.3 MU1 firmware
+                 and hp3parclient 3.1.0
 
     """
 
-    VERSION = "2.0.16"
+    VERSION = "2.0.17"
 
     stats = {}
 
index e8374814d2b347c7dadcdd430935c13935258aa9..8f08bf41e1ab58b59c6d84f9c3d0ec3157adb539 100644 (file)
@@ -19,7 +19,7 @@ This driver requires 3.1.3 firmware on the 3PAR array, using
 the 3.x version of the hp3parclient.
 
 You will need to install the python hp3parclient.
-sudo pip install --upgrade "hp3parclient>=3.0"
+sudo pip install --upgrade "hp3parclient>=3.1"
 
 Set the following in the cinder.conf file to enable the
 3PAR iSCSI Driver along with the required flags:
@@ -27,6 +27,7 @@ Set the following in the cinder.conf file to enable the
 volume_driver=cinder.volume.drivers.san.hp.hp_3par_iscsi.HP3PARISCSIDriver
 """
 
+import re
 import sys
 
 try:
@@ -41,9 +42,12 @@ from cinder import utils
 import cinder.volume.driver
 from cinder.volume.drivers.san.hp import hp_3par_common as hpcommon
 from cinder.volume.drivers.san import san
+from cinder.volume import utils as volume_utils
 
 LOG = logging.getLogger(__name__)
 DEFAULT_ISCSI_PORT = 3260
+CHAP_USER_KEY = "HPQ-cinder-CHAP-name"
+CHAP_PASS_KEY = "HPQ-cinder-CHAP-secret"
 
 
 class HP3PARISCSIDriver(cinder.volume.driver.ISCSIDriver):
@@ -67,10 +71,12 @@ class HP3PARISCSIDriver(cinder.volume.driver.ISCSIDriver):
         2.0.2 - Add back-end assisted volume migrate
         2.0.3 - Added support for managing/unmanaging of volumes
         2.0.4 - Added support for volume retype
+        2.0.5 - Added CHAP support, requires 3.1.3 MU1 firmware
+                and hp3parclient 3.1.0.
 
     """
 
-    VERSION = "2.0.4"
+    VERSION = "2.0.5"
 
     def __init__(self, *args, **kwargs):
         super(HP3PARISCSIDriver, self).__init__(*args, **kwargs)
@@ -263,9 +269,8 @@ class HP3PARISCSIDriver(cinder.volume.driver.ISCSIDriver):
         """
         self.common.client_login()
         try:
-
             # we have to make sure we have a host
-            host = self._create_host(volume, connector)
+            host, username, password = self._create_host(volume, connector)
             least_used_nsp = self._get_least_used_nsp_for_host(host['name'])
 
             # now that we have a host, create the VLUN
@@ -289,6 +294,12 @@ class HP3PARISCSIDriver(cinder.volume.driver.ISCSIDriver):
                              'target_discovered': True
                              }
                     }
+
+            if self.configuration.hp3par_iscsi_chap_enabled:
+                info['data']['auth_method'] = 'CHAP'
+                info['data']['auth_username'] = username
+                info['data']['auth_password'] = password
+
             return info
         finally:
             self.common.client_logout()
@@ -301,9 +312,31 @@ class HP3PARISCSIDriver(cinder.volume.driver.ISCSIDriver):
             hostname = self.common._safe_hostname(connector['host'])
             self.common.terminate_connection(volume, hostname,
                                              iqn=connector['initiator'])
+            self._clear_chap_3par(volume)
         finally:
             self.common.client_logout()
 
+    def _clear_chap_3par(self, volume):
+        """Clears CHAP credentials on a 3par volume.
+
+        Ignore exceptions caused by the keys not being present on a volume.
+        """
+        vol_name = self.common._get_3par_vol_name(volume['id'])
+
+        try:
+            self.common.client.removeVolumeMetaData(vol_name, CHAP_USER_KEY)
+        except hpexceptions.HTTPNotFound:
+            pass
+        except Exception:
+            raise
+
+        try:
+            self.common.client.removeVolumeMetaData(vol_name, CHAP_PASS_KEY)
+        except hpexceptions.HTTPNotFound:
+            pass
+        except Exception:
+            raise
+
     def _create_3par_iscsi_host(self, hostname, iscsi_iqn, domain, persona_id):
         """Create a 3PAR host.
 
@@ -333,18 +366,37 @@ class HP3PARISCSIDriver(cinder.volume.driver.ISCSIDriver):
 
         self.common.client.modifyHost(hostname, mod_request)
 
+    def _set_3par_chaps(self, hostname, volume, username, password):
+        """Sets a 3PAR host's CHAP credentials."""
+        if not self.configuration.hp3par_iscsi_chap_enabled:
+            return
+
+        mod_request = {'chapOperation': self.common.client.HOST_EDIT_ADD,
+                       'chapOperationMode': self.common.client.CHAP_INITIATOR,
+                       'chapName': username,
+                       'chapSecret': password}
+        self.common.client.modifyHost(hostname, mod_request)
+
     def _create_host(self, volume, connector):
         """Creates or modifies existing 3PAR host."""
         # make sure we don't have the host already
         host = None
+        username = None
+        password = None
         hostname = self.common._safe_hostname(connector['host'])
         cpg = self.common.get_cpg(volume, allowSnap=True)
         domain = self.common.get_domain(cpg)
+
+        # Get the CHAP secret if CHAP is enabled
+        if self.configuration.hp3par_iscsi_chap_enabled:
+            vol_name = self.common._get_3par_vol_name(volume['id'])
+            username = self.common.client.getVolumeMetaData(
+                vol_name, CHAP_USER_KEY)['value']
+            password = self.common.client.getVolumeMetaData(
+                vol_name, CHAP_PASS_KEY)['value']
+
         try:
             host = self.common._get_3par_host(hostname)
-            if 'iSCSIPaths' not in host or len(host['iSCSIPaths']) < 1:
-                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)
@@ -353,17 +405,134 @@ class HP3PARISCSIDriver(cinder.volume.driver.ISCSIDriver):
                                                     connector['initiator'],
                                                     domain,
                                                     persona_id)
+            self._set_3par_chaps(hostname, volume, username, password)
             host = self.common._get_3par_host(hostname)
+        else:
+            if 'iSCSIPaths' not in host or len(host['iSCSIPaths']) < 1:
+                self._modify_3par_iscsi_host(hostname, connector['initiator'])
+                self._set_3par_chaps(hostname, volume, username, password)
+                host = self.common._get_3par_host(hostname)
+            elif (not host['initiatorChapEnabled'] and
+                    self.configuration.hp3par_iscsi_chap_enabled):
+                LOG.warn(_("Host exists without CHAP credentials set and has "
+                           "iSCSI attachments but CHAP is enabled.  Updating "
+                           "host with new CHAP credentials."))
+                self._set_3par_chaps(hostname, volume, username, password)
+
+        return host, username, password
 
-        return host
+    def _do_export(self, volume):
+        """Gets the associated account, generates CHAP info and updates."""
+        model_update = {}
+
+        if not self.configuration.hp3par_iscsi_chap_enabled:
+            model_update['provider_auth'] = None
+            return model_update
+
+        # CHAP username will be the hostname
+        chap_username = volume['host'].split('@')[0]
+
+        chap_password = None
+        try:
+            # Get all active VLUNs for the host
+            vluns = self.common.client.getHostVLUNs(chap_username)
+
+            # Host has active VLUNs... is CHAP enabled on host?
+            host_info = self.common.client.getHost(chap_username)
+
+            if not host_info['initiatorChapEnabled']:
+                LOG.warn(_("Host has no CHAP key, but CHAP is enabled."))
+
+        except hpexceptions.HTTPNotFound:
+            chap_password = volume_utils.generate_password(16)
+            LOG.warn(_("No host or VLUNs exist. Generating new CHAP key."))
+        else:
+            # Get a list of all iSCSI VLUNs and see if there is already a CHAP
+            # key assigned to one of them.  Use that CHAP key if present,
+            # otherwise create a new one.  Skip any VLUNs that are missing
+            # CHAP credentials in metadata.
+            chap_exists = False
+            active_vluns = 0
+
+            for vlun in vluns:
+                if not vlun['active']:
+                    continue
+
+                active_vluns += 1
+
+                # iSCSI connections start with 'iqn'.
+                if ('remoteName' in vlun and
+                        re.match('iqn.*', vlun['remoteName'])):
+                    try:
+                        chap_password = self.common.client.getVolumeMetaData(
+                            vlun['volumeName'], CHAP_PASS_KEY)['value']
+                        chap_exists = True
+                        break
+                    except hpexceptions.HTTPNotFound:
+                        LOG.debug("The VLUN %s is missing CHAP credentials "
+                                  "but CHAP is enabled. Skipping." %
+                                  vlun['remoteName'])
+                else:
+                    LOG.warn(_("Non-iSCSI VLUN detected."))
+
+            if not chap_exists:
+                chap_password = volume_utils.generate_password(16)
+                LOG.warn(_("No VLUN contained CHAP credentials. "
+                           "Generating new CHAP key."))
+
+        # Add CHAP credentials to the volume metadata
+        vol_name = self.common._get_3par_vol_name(volume['id'])
+        self.common.client.setVolumeMetaData(
+            vol_name, CHAP_USER_KEY, chap_username)
+        self.common.client.setVolumeMetaData(
+            vol_name, CHAP_PASS_KEY, chap_password)
+
+        model_update['provider_auth'] = ('CHAP %s %s' %
+                                         (chap_username, chap_password))
+
+        return model_update
 
     @utils.synchronized('3par', external=True)
     def create_export(self, context, volume):
-        pass
+        try:
+            self.common.client_login()
+            return self._do_export(volume)
+        finally:
+            self.common.client_logout()
 
     @utils.synchronized('3par', external=True)
     def ensure_export(self, context, volume):
-        pass
+        """Ensure the volume still exists on the 3PAR.
+
+        Also retrieves CHAP credentials, if present on the volume
+        """
+        try:
+            self.common.client_login()
+            vol_name = self.common._get_3par_vol_name(volume['id'])
+            self.common.client.getVolume(vol_name)
+        except hpexceptions.HTTPNotFound:
+            LOG.error(_("Volume %s doesn't exist on array.") % vol_name)
+        else:
+            metadata = self.common.client.getAllVolumeMetaData(vol_name)
+
+            username = None
+            password = None
+            model_update = {}
+            model_update['provider_auth'] = None
+
+            for member in metadata['members']:
+                if member['key'] == CHAP_USER_KEY:
+                    username = member['value']
+                elif member['key'] == CHAP_PASS_KEY:
+                    password = member['value']
+
+            if username and password:
+                model_update['provider_auth'] = ('CHAP %s %s' %
+                                                 (username, password))
+
+            return model_update
+        finally:
+            self.common.client_logout()
 
     @utils.synchronized('3par', external=True)
     def remove_export(self, context, volume):
index 38af8e7cbad76da239feaafbc3915a7dd77caa24..2a13d31c7147ed7a96208c0734ba043fdc31cc08 100644 (file)
 # List of target iSCSI addresses to use. (list value)
 #hp3par_iscsi_ips=
 
+# Enable CHAP authentication for iSCSI connections. (boolean
+# value)
+#hp3par_iscsi_chap_enabled=false
+
 
 #
 # Options defined in cinder.volume.drivers.san.hp.hp_lefthand_rest_proxy