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):
'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]
}
configuration.hp3par_snapshot_expiration = ""
configuration.hp3par_snapshot_retention = ""
configuration.hp3par_iscsi_ips = []
+ configuration.hp3par_iscsi_chap_enabled = False
return configuration
@mock.patch(
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)
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),
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
{'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'),
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
'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'),
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):
['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},
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:
volume_driver=cinder.volume.drivers.san.hp.hp_3par_iscsi.HP3PARISCSIDriver
"""
+import re
import sys
try:
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):
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)
"""
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
'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()
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.
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)
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):