3PAR: Add Peer Persistence support

Background:
Given 3PAR backend configured with replication setup, currently only
Active/Passive replication is supported by 3PAR in OpenStack. When
failover happens, nova does not support volume force-detach (from
dead primary backend) / re-attach to secondary backend. Storage
engineer's manual intervention is required.

Proposed Solution:
Given a system with Peer Persistence configured and replicated volume
is created. When this volume is attached to an instance, vlun would be
created automatically in secondary backend, in addition to primary
backend. So that when a failover happens, it is seamless.

This solution would apply under following conditions:
1] multipath is enabled
2] replication mode is set as "sync"
3] quorum witness is configured

Closes bug: #1839140

Change-Id: I2d0342adace69314bd159d3d6415a87ff29db562
This commit is contained in:
raghavendrat 2019-08-22 03:06:44 -07:00
parent 7bb05f327a
commit 46a16f1d61
7 changed files with 1063 additions and 204 deletions

View File

@ -7278,6 +7278,133 @@ class TestHPE3PARFCDriver(HPE3PARBaseDriver):
self.standard_logout)
self.assertDictEqual(expected_properties, result)
@mock.patch.object(volume_types, 'get_volume_type')
def test_initialize_connection_peer_persistence(self, _mock_volume_types):
# setup_mock_client drive with default configuration
# and return the mock HTTP 3PAR client
conf = self.setup_configuration()
self.replication_targets[0]['replication_mode'] = 'sync'
self.replication_targets[0]['quorum_witness_ip'] = '10.50.3.192'
conf.replication_device = self.replication_targets
mock_client = self.setup_driver(config=conf)
mock_client.getStorageSystemInfo.return_value = (
{'id': self.CLIENT_ID})
mock_replicated_client = self.setup_driver(config=conf)
mock_replicated_client.getStorageSystemInfo.return_value = (
{'id': self.REPLICATION_CLIENT_ID})
_mock_volume_types.return_value = {
'name': 'replicated',
'extra_specs': {
'replication_enabled': '<is> True',
'replication:mode': 'sync',
'volume_type': self.volume_type_replicated}}
mock_client.getVolume.return_value = {'userCPG': HPE3PAR_CPG}
mock_client.getCPG.return_value = {}
mock_client.getHost.side_effect = [
hpeexceptions.HTTPNotFound('fake'),
{'name': self.FAKE_HOST,
'FCPaths': [{'driverVersion': None,
'firmwareVersion': None,
'hostSpeed': 0,
'model': None,
'portPos': {'cardPort': 1, 'node': 7,
'slot': 1},
'vendor': None,
'wwn': self.wwn[0]},
{'driverVersion': None,
'firmwareVersion': None,
'hostSpeed': 0,
'model': None,
'portPos': {'cardPort': 1, 'node': 6,
'slot': 1},
'vendor': None,
'wwn': self.wwn[1]}]}]
mock_client.queryHost.return_value = {
'members': [{
'name': self.FAKE_HOST
}]
}
mock_client.getHostVLUNs.side_effect = [
hpeexceptions.HTTPNotFound('fake'),
[{'active': True,
'volumeName': self.VOLUME_3PAR_NAME,
'remoteName': self.wwn[1],
'lun': 90, 'type': 0}],
[{'active': True,
'volumeName': self.VOLUME_3PAR_NAME,
'remoteName': self.wwn[0],
'lun': 90, 'type': 0}]]
mock_replicated_client.getHostVLUNs.side_effect = (
mock_client.getHostVLUNs.side_effect)
location = ("%(volume_name)s,%(lun_id)s,%(host)s,%(nsp)s" %
{'volume_name': self.VOLUME_3PAR_NAME,
'lun_id': 90,
'host': self.FAKE_HOST,
'nsp': 'something'})
mock_client.createVLUN.return_value = location
mock_replicated_client.createVLUN.return_value = location
expected_properties = {
'driver_volume_type': 'fibre_channel',
'data': {
'encrypted': False,
'target_lun': 90,
'target_wwn': ['0987654321234', '123456789000987',
'0987654321234', '123456789000987'],
'target_discovered': True,
'initiator_target_map':
{'123456789012345': ['0987654321234', '123456789000987',
'0987654321234', '123456789000987'],
'123456789054321': ['0987654321234', '123456789000987',
'0987654321234', '123456789000987']}}}
with mock.patch.object(
hpecommon.HPE3PARCommon,
'_create_client') as mock_create_client, \
mock.patch.object(
hpecommon.HPE3PARCommon,
'_create_replication_client') as mock_replication_client:
mock_create_client.return_value = mock_client
mock_replication_client.return_value = mock_replicated_client
volume = self.volume
volume['replication_status'] = 'enabled'
result = self.driver.initialize_connection(
volume,
self.connector_multipath_enabled)
expected = [
mock.call.getVolume(self.VOLUME_3PAR_NAME),
mock.call.getCPG(HPE3PAR_CPG),
mock.call.getHost(self.FAKE_HOST),
mock.call.queryHost(wwns=['123456789012345',
'123456789054321']),
mock.call.getHost(self.FAKE_HOST),
mock.call.getPorts(),
mock.call.getHostVLUNs(self.FAKE_HOST),
mock.call.createVLUN(
self.VOLUME_3PAR_NAME,
auto=True,
hostname=self.FAKE_HOST,
lun=None),
mock.call.getHostVLUNs(self.FAKE_HOST)]
mock_client.assert_has_calls(
self.get_id_login +
self.standard_logout +
self.standard_login +
expected +
self.standard_logout)
self.assertDictEqual(expected_properties, result)
def test_terminate_connection(self):
# setup_mock_client drive with default configuration
# and return the mock HTTP 3PAR client
@ -7470,6 +7597,8 @@ class TestHPE3PARFCDriver(HPE3PARBaseDriver):
def test_terminate_connection_more_vols(self):
mock_client = self.setup_driver()
mock_client.getStorageSystemInfo.return_value = (
{'id': self.CLIENT_ID})
# mock more than one vlun on the host (don't even try to remove host)
mock_client.getHostVLUNs.return_value = \
[
@ -7519,11 +7648,141 @@ class TestHPE3PARFCDriver(HPE3PARBaseDriver):
conn_info = self.driver.terminate_connection(self.volume,
self.connector)
mock_client.assert_has_calls(
self.get_id_login +
self.standard_logout +
self.standard_login +
expect_less +
self.standard_logout)
self.assertEqual(expect_conn, conn_info)
@mock.patch.object(volume_types, 'get_volume_type')
def test_terminate_connection_peer_persistence(self, _mock_volume_types):
# setup_mock_client drive with default configuration
# and return the mock HTTP 3PAR client
conf = self.setup_configuration()
self.replication_targets[0]['replication_mode'] = 'sync'
self.replication_targets[0]['quorum_witness_ip'] = '10.50.3.192'
conf.replication_device = self.replication_targets
mock_client = self.setup_driver(config=conf)
mock_client.getStorageSystemInfo.return_value = (
{'id': self.CLIENT_ID})
mock_replicated_client = self.setup_driver(config=conf)
mock_replicated_client.getStorageSystemInfo.return_value = (
{'id': self.REPLICATION_CLIENT_ID})
_mock_volume_types.return_value = {
'name': 'replicated',
'extra_specs': {
'replication_enabled': '<is> True',
'replication:mode': 'sync',
'volume_type': self.volume_type_replicated}}
effects = [
[{'active': False, 'volumeName': self.VOLUME_3PAR_NAME,
'lun': None, 'type': 0}],
hpeexceptions.HTTPNotFound,
hpeexceptions.HTTPNotFound]
mock_client.getHostVLUNs.side_effect = effects
mock_replicated_client.getHostVLUNs.side_effect = effects
getHost_side_effect = [
hpeexceptions.HTTPNotFound('fake'),
{'name': self.FAKE_HOST,
'FCPaths': [{'driverVersion': None,
'firmwareVersion': None,
'hostSpeed': 0,
'model': None,
'portPos': {'cardPort': 1, 'node': 7,
'slot': 1},
'vendor': None,
'wwn': self.wwn[0]},
{'driverVersion': None,
'firmwareVersion': None,
'hostSpeed': 0,
'model': None,
'portPos': {'cardPort': 1, 'node': 6,
'slot': 1},
'vendor': None,
'wwn': self.wwn[1]}]}]
queryHost_return_value = {
'members': [{
'name': self.FAKE_HOST
}]
}
mock_client.getHost.side_effect = getHost_side_effect
mock_client.queryHost.return_value = queryHost_return_value
mock_replicated_client.getHost.side_effect = getHost_side_effect
mock_replicated_client.queryHost.return_value = queryHost_return_value
expected = [
mock.call.queryHost(wwns=['123456789012345', '123456789054321']),
mock.call.getHostVLUNs(self.FAKE_HOST),
mock.call.deleteVLUN(
self.VOLUME_3PAR_NAME,
None,
hostname=self.FAKE_HOST),
mock.call.getHostVLUNs(self.FAKE_HOST),
mock.call.deleteHost(self.FAKE_HOST),
mock.call.getHostVLUNs(self.FAKE_HOST),
mock.call.getPorts()]
volume = self.volume
volume['replication_status'] = 'enabled'
with mock.patch.object(
hpecommon.HPE3PARCommon,
'_create_client') as mock_create_client, \
mock.patch.object(
hpecommon.HPE3PARCommon,
'_create_replication_client') as mock_replication_client:
mock_create_client.return_value = mock_client
mock_replication_client.return_value = mock_replicated_client
conn_info = self.driver.terminate_connection(
volume, self.connector_multipath_enabled)
mock_client.assert_has_calls(
self.standard_login +
expected +
self.standard_logout)
self.assertIn('data', conn_info)
self.assertIn('initiator_target_map', conn_info['data'])
mock_client.reset_mock()
mock_replicated_client.reset_mock()
mock_client.getHostVLUNs.side_effect = effects
mock_replicated_client.getHostVLUNs.side_effect = effects
# mock some deleteHost exceptions that are handled
delete_with_vlun = hpeexceptions.HTTPConflict(
error={'message': "has exported VLUN"})
delete_with_hostset = hpeexceptions.HTTPConflict(
error={'message': "host is a member of a set"})
mock_client.deleteHost = mock.Mock(
side_effect=[delete_with_vlun, delete_with_hostset])
conn_info = self.driver.terminate_connection(
volume, self.connector_multipath_enabled)
mock_client.assert_has_calls(
self.standard_login +
expected +
self.standard_logout)
mock_client.reset_mock()
mock_replicated_client.reset_mock()
mock_client.getHostVLUNs.side_effect = effects
mock_replicated_client.getHostVLUNs.side_effect = effects
conn_info = self.driver.terminate_connection(
volume, self.connector_multipath_enabled)
mock_client.assert_has_calls(
self.standard_login +
expected +
self.standard_logout)
def test_get_3par_host_from_wwn_iqn(self):
mock_client = self.setup_driver()
mock_client.getHosts.return_value = {
@ -7954,7 +8213,7 @@ class TestHPE3PARFCDriver(HPE3PARBaseDriver):
'_create_client') as mock_create_client:
mock_create_client.return_value = mock_client
common = self.driver._login()
host = self.driver._create_host(
host, cpg = self.driver._create_host(
common,
self.volume,
self.connector_multipath_enabled)
@ -7969,6 +8228,7 @@ class TestHPE3PARFCDriver(HPE3PARBaseDriver):
mock_client.assert_has_calls(expected)
self.assertEqual('fake', host['name'])
self.assertEqual(HPE3PAR_CPG, cpg)
def test_create_host(self):
# setup_mock_client drive with default configuration
@ -8001,7 +8261,7 @@ class TestHPE3PARFCDriver(HPE3PARBaseDriver):
'_create_client') as mock_create_client:
mock_create_client.return_value = mock_client
common = self.driver._login()
host = self.driver._create_host(
host, cpg = self.driver._create_host(
common,
self.volume,
self.connector_multipath_enabled)
@ -8043,7 +8303,7 @@ class TestHPE3PARFCDriver(HPE3PARBaseDriver):
'_create_client') as mock_create_client:
mock_create_client.return_value = mock_client
common = self.driver._login()
host = self.driver._create_host(
host, cpg = self.driver._create_host(
common,
self.volume,
self.connector_multipath_enabled)
@ -8083,7 +8343,7 @@ class TestHPE3PARFCDriver(HPE3PARBaseDriver):
'_create_client') as mock_create_client:
mock_create_client.return_value = mock_client
common = self.driver._login()
host = self.driver._create_host(
host, cpg = self.driver._create_host(
common,
self.volume,
self.connector_multipath_enabled)
@ -8122,7 +8382,7 @@ class TestHPE3PARFCDriver(HPE3PARBaseDriver):
'_create_client') as mock_create_client:
mock_create_client.return_value = mock_client
common = self.driver._login()
host = self.driver._create_host(
host, cpg = self.driver._create_host(
common,
self.volume,
self.connector_multipath_enabled)
@ -8175,7 +8435,7 @@ class TestHPE3PARFCDriver(HPE3PARBaseDriver):
'_create_client') as mock_create_client:
mock_create_client.return_value = mock_client
common = self.driver._login()
host = self.driver._create_host(
host, cpg = self.driver._create_host(
common,
self.volume,
self.connector_multipath_enabled)
@ -8218,7 +8478,7 @@ class TestHPE3PARFCDriver(HPE3PARBaseDriver):
'_create_client') as mock_create_client:
mock_create_client.return_value = mock_client
common = self.driver._login()
host = self.driver._create_host(
host, cpg = self.driver._create_host(
common,
self.volume,
self.connector_multipath_enabled)
@ -8417,8 +8677,10 @@ class TestHPE3PARISCSIDriver(HPE3PARBaseDriver):
with mock.patch.object(hpecommon.HPE3PARCommon,
'_create_client') as mock_create_client:
mock_create_client.return_value = mock_client
volume = self.volume
volume['replication_status'] = 'disabled'
result = self.driver.initialize_connection(
self.volume,
volume,
self.connector_multipath_enabled)
expected = [
@ -8477,8 +8739,10 @@ class TestHPE3PARISCSIDriver(HPE3PARBaseDriver):
with mock.patch.object(hpecommon.HPE3PARCommon,
'_create_client') as mock_create_client:
mock_create_client.return_value = mock_client
volume = self.volume
volume['replication_status'] = 'disabled'
result = self.driver.initialize_connection(
self.volume,
volume,
self.connector_multipath_enabled)
expected = [
@ -8553,6 +8817,111 @@ class TestHPE3PARISCSIDriver(HPE3PARBaseDriver):
expected_properties['data']['encrypted'] = True
self.assertDictEqual(self.properties, result)
@mock.patch.object(volume_types, 'get_volume_type')
def test_initialize_connection_peer_persistence(self, _mock_volume_types):
# setup_mock_client drive with default configuration
# and return the mock HTTP 3PAR client
conf = self.setup_configuration()
self.replication_targets[0]['replication_mode'] = 'sync'
self.replication_targets[0]['quorum_witness_ip'] = '10.50.3.192'
self.replication_targets[0]['hpe3par_iscsi_ips'] = '1.1.1.2'
conf.replication_device = self.replication_targets
mock_client = self.setup_driver(config=conf)
mock_client.getStorageSystemInfo.return_value = (
{'id': self.CLIENT_ID})
mock_replicated_client = self.setup_driver(config=conf)
mock_replicated_client.getStorageSystemInfo.return_value = (
{'id': self.REPLICATION_CLIENT_ID})
_mock_volume_types.return_value = {
'name': 'replicated',
'extra_specs': {
'replication_enabled': '<is> True',
'replication:mode': 'sync',
'volume_type': self.volume_type_replicated}}
mock_client.getVolume.return_value = {'userCPG': HPE3PAR_CPG}
mock_client.getCPG.return_value = {}
mock_client.getHost.side_effect = [
hpeexceptions.HTTPNotFound('fake'),
{'name': self.FAKE_HOST}]
mock_client.queryHost.return_value = {
'members': [{
'name': self.FAKE_HOST
}]
}
mock_client.getHostVLUNs.side_effect = [
hpeexceptions.HTTPNotFound('fake'),
[{'active': True,
'volumeName': self.VOLUME_3PAR_NAME,
'lun': self.TARGET_LUN, 'type': 0,
'portPos': {'node': 8, 'slot': 1, 'cardPort': 1}}]]
mock_replicated_client.getHostVLUNs.side_effect = [
hpeexceptions.HTTPNotFound('fake'),
[{'active': True,
'volumeName': self.VOLUME_3PAR_NAME,
'lun': self.TARGET_LUN, 'type': 0,
'portPos': {'node': 8, 'slot': 1, 'cardPort': 1}}]]
location = ("%(volume_name)s,%(lun_id)s,%(host)s,%(nsp)s" %
{'volume_name': self.VOLUME_3PAR_NAME,
'lun_id': self.TARGET_LUN,
'host': self.FAKE_HOST,
'nsp': 'something'})
mock_client.createVLUN.return_value = location
mock_replicated_client.createVLUN.return_value = location
mock_client.getiSCSIPorts.return_value = [{
'IPAddr': '1.1.1.2',
'iSCSIName': self.TARGET_IQN,
}]
with mock.patch.object(
hpecommon.HPE3PARCommon,
'_create_client') as mock_create_client, \
mock.patch.object(
hpecommon.HPE3PARCommon,
'_create_replication_client') as mock_replication_client:
mock_create_client.return_value = mock_client
mock_replication_client.return_value = mock_replicated_client
volume = self.volume
volume['replication_status'] = 'enabled'
result = self.driver.initialize_connection(
volume,
self.connector_multipath_enabled)
expected = [
mock.call.getVolume(self.VOLUME_3PAR_NAME),
mock.call.getCPG(HPE3PAR_CPG),
mock.call.getHost(self.FAKE_HOST),
mock.call.queryHost(iqns=['iqn.1993-08.org.debian:01:222']),
mock.call.getHost(self.FAKE_HOST),
mock.call.getiSCSIPorts(state=4),
mock.call.getHostVLUNs(self.FAKE_HOST),
mock.call.createVLUN(
self.VOLUME_3PAR_NAME,
auto=True,
hostname=self.FAKE_HOST,
portPos=self.FAKE_ISCSI_PORT['portPos'],
lun=None),
mock.call.getHostVLUNs(self.FAKE_HOST)]
mock_client.assert_has_calls(
self.get_id_login +
self.standard_logout +
self.standard_login +
expected +
self.standard_logout)
self.assertDictEqual(self.multipath_properties, result)
def test_terminate_connection_for_clear_chap_creds_not_found(self):
# setup_mock_client drive with default configuration
# and return the mock HTTP 3PAR client
@ -9050,7 +9419,7 @@ class TestHPE3PARISCSIDriver(HPE3PARBaseDriver):
'_create_client') as mock_create_client:
mock_create_client.return_value = mock_client
common = self.driver._login()
host, auth_username, auth_password = self.driver._create_host(
host, auth_username, auth_password, cpg = self.driver._create_host(
common, self.volume, self.connector)
expected = [
mock.call.getVolume('osv-0DM4qZEVSKON-DXN-NwVpw'),
@ -9080,7 +9449,7 @@ class TestHPE3PARISCSIDriver(HPE3PARBaseDriver):
'_create_client') as mock_create_client:
mock_create_client.return_value = mock_client
common = self.driver._login()
host, auth_username, auth_password = self.driver._create_host(
host, auth_username, auth_password, cpg = self.driver._create_host(
common, self.volume, self.connector)
expected = [
mock.call.getVolume('osv-0DM4qZEVSKON-DXN-NwVpw'),
@ -9098,6 +9467,7 @@ class TestHPE3PARISCSIDriver(HPE3PARBaseDriver):
self.assertEqual(self.FAKE_HOST, host['name'])
self.assertIsNone(auth_username)
self.assertIsNone(auth_password)
self.assertEqual(HPE3PAR_CPG, cpg)
def test_create_host_chap_enabled(self):
# setup_mock_client drive with CHAP enabled configuration
@ -9135,7 +9505,7 @@ class TestHPE3PARISCSIDriver(HPE3PARBaseDriver):
'_create_client') as mock_create_client:
mock_create_client.return_value = mock_client
common = self.driver._login()
host, auth_username, auth_password = self.driver._create_host(
host, auth_username, auth_password, cpg = self.driver._create_host(
common, self.volume, self.connector)
expected = [
mock.call.getVolume('osv-0DM4qZEVSKON-DXN-NwVpw'),
@ -9202,7 +9572,7 @@ class TestHPE3PARISCSIDriver(HPE3PARBaseDriver):
'_create_client') as mock_create_client:
mock_create_client.return_value = mock_client
common = self.driver._login()
host, auth_username, auth_password = self.driver._create_host(
host, auth_username, auth_password, cpg = self.driver._create_host(
common, self.volume, self.connector)
expected = [
@ -9264,7 +9634,7 @@ class TestHPE3PARISCSIDriver(HPE3PARBaseDriver):
'_create_client') as mock_create_client:
mock_create_client.return_value = mock_client
common = self.driver._login()
host, auth_username, auth_password = self.driver._create_host(
host, auth_username, auth_password, cpg = self.driver._create_host(
common, self.volume, self.connector)
expected = [
@ -9299,7 +9669,7 @@ class TestHPE3PARISCSIDriver(HPE3PARBaseDriver):
'_create_client') as mock_create_client:
mock_create_client.return_value = mock_client
common = self.driver._login()
host, auth_username, auth_password = self.driver._create_host(
host, auth_username, auth_password, cpg = self.driver._create_host(
common, self.volume, self.connector)
expected = [
@ -9354,7 +9724,7 @@ class TestHPE3PARISCSIDriver(HPE3PARBaseDriver):
'_create_client') as mock_create_client:
mock_create_client.return_value = mock_client
common = self.driver._login()
host, auth_username, auth_password = self.driver._create_host(
host, auth_username, auth_password, cpg = self.driver._create_host(
common, self.volume, self.connector)
expected = [
@ -9399,7 +9769,7 @@ class TestHPE3PARISCSIDriver(HPE3PARBaseDriver):
'_create_client') as mock_create_client:
mock_create_client.return_value = mock_client
common = self.driver._login()
host, user, pwd = self.driver._create_host(
host, user, pwd, cpg = self.driver._create_host(
common, self.volume, self.connector)
expected = [
mock.call.getVolume('osv-0DM4qZEVSKON-DXN-NwVpw'),
@ -9433,7 +9803,7 @@ class TestHPE3PARISCSIDriver(HPE3PARBaseDriver):
'_create_client') as mock_create_client:
mock_create_client.return_value = mock_client
common = self.driver._login()
host, auth_username, auth_password = self.driver._create_host(
host, auth_username, auth_password, cpg = self.driver._create_host(
common, self.volume, self.connector)
expected = [
@ -9491,7 +9861,7 @@ class TestHPE3PARISCSIDriver(HPE3PARBaseDriver):
'_create_client') as mock_create_client:
mock_create_client.return_value = mock_client
common = self.driver._login()
host, auth_username, auth_password = self.driver._create_host(
host, auth_username, auth_password, cpg = self.driver._create_host(
common, self.volume, self.connector)
expected = [
@ -10295,6 +10665,80 @@ class TestHPE3PARISCSIDriver(HPE3PARBaseDriver):
expected +
self.standard_logout)
@mock.patch.object(volume_types, 'get_volume_type')
def test_terminate_connection_peer_persistence(self, _mock_volume_types):
# setup_mock_client drive with default configuration
# and return the mock HTTP 3PAR client
conf = self.setup_configuration()
self.replication_targets[0]['replication_mode'] = 'sync'
self.replication_targets[0]['quorum_witness_ip'] = '10.50.3.192'
conf.replication_device = self.replication_targets
mock_client = self.setup_driver(config=conf)
mock_client.getStorageSystemInfo.return_value = (
{'id': self.CLIENT_ID})
mock_replicated_client = self.setup_driver(config=conf)
mock_replicated_client.getStorageSystemInfo.return_value = (
{'id': self.REPLICATION_CLIENT_ID})
_mock_volume_types.return_value = {
'name': 'replicated',
'extra_specs': {
'replication_enabled': '<is> True',
'replication:mode': 'sync',
'volume_type': self.volume_type_replicated}}
mock_client.getHostVLUNs.return_value = [
{'active': False,
'volumeName': self.VOLUME_3PAR_NAME,
'lun': None, 'type': 0}]
mock_client.queryHost.return_value = {
'members': [{
'name': self.FAKE_HOST
}]
}
volume = self.volume
volume['replication_status'] = 'enabled'
with mock.patch.object(
hpecommon.HPE3PARCommon,
'_create_client') as mock_create_client, \
mock.patch.object(
hpecommon.HPE3PARCommon,
'_create_replication_client') as mock_replication_client:
mock_create_client.return_value = mock_client
mock_replication_client.return_value = mock_replicated_client
self.driver.terminate_connection(
volume,
self.connector_multipath_enabled)
expected = [
mock.call.queryHost(iqns=[self.connector['initiator']]),
mock.call.getHostVLUNs(self.FAKE_HOST),
mock.call.deleteVLUN(
self.VOLUME_3PAR_NAME,
None,
hostname=self.FAKE_HOST),
mock.call.getHostVLUNs(self.FAKE_HOST),
mock.call.modifyHost(
'fakehost',
{'pathOperation': 2,
'iSCSINames': ['iqn.1993-08.org.debian:01:222']}),
mock.call.removeVolumeMetaData(
self.VOLUME_3PAR_NAME, CHAP_USER_KEY),
mock.call.removeVolumeMetaData(
self.VOLUME_3PAR_NAME, CHAP_PASS_KEY)]
mock_client.assert_has_calls(
self.standard_login +
expected +
self.standard_logout)
VLUNS5_RET = ({'members':
[{'portPos': {'node': 0, 'slot': 8, 'cardPort': 2},

View File

@ -289,11 +289,12 @@ class HPE3PARCommon(object):
4.0.12 - Added multiattach support
4.0.13 - Fixed detaching issue for volume with type multiattach
enabled. bug #1834660
4.0.14 - Added Peer Persistence feature
"""
VERSION = "4.0.13"
VERSION = "4.0.14"
stats = {}
@ -1374,8 +1375,8 @@ class HPE3PARCommon(object):
capacity = int(math.ceil(capacity / units.Mi))
return capacity
def _delete_3par_host(self, hostname):
self.client.deleteHost(hostname)
def _delete_3par_host(self, hostname, client_obj):
client_obj.deleteHost(hostname)
def _get_prioritized_host_on_3par(self, host, hosts, hostname):
# Check whether host with wwn/iqn of initiator present on 3par
@ -1396,7 +1397,8 @@ class HPE3PARCommon(object):
return host, hostname
def _create_3par_vlun(self, volume, hostname, nsp, lun_id=None):
def _create_3par_vlun(self, volume, hostname, nsp, lun_id=None,
remote_client=None):
try:
location = None
auto = True
@ -1404,14 +1406,19 @@ class HPE3PARCommon(object):
if lun_id is not None:
auto = False
if remote_client:
client_obj = remote_client
else:
client_obj = self.client
if nsp is None:
location = self.client.createVLUN(volume, hostname=hostname,
auto=auto, lun=lun_id)
location = client_obj.createVLUN(volume, hostname=hostname,
auto=auto, lun=lun_id)
else:
port = self.build_portPos(nsp)
location = self.client.createVLUN(volume, hostname=hostname,
auto=auto, portPos=port,
lun=lun_id)
location = client_obj.createVLUN(volume, hostname=hostname,
auto=auto, portPos=port,
lun=lun_id)
vlun_info = None
if location:
@ -1454,33 +1461,49 @@ class HPE3PARCommon(object):
def get_ports(self):
return self.client.getPorts()
def get_active_target_ports(self):
ports = self.get_ports()
def get_active_target_ports(self, remote_client=None):
if remote_client:
client_obj = remote_client
ports = remote_client.getPorts()
else:
client_obj = self.client
ports = self.get_ports()
target_ports = []
for port in ports['members']:
if (
port['mode'] == self.client.PORT_MODE_TARGET and
port['linkState'] == self.client.PORT_STATE_READY
port['mode'] == client_obj.PORT_MODE_TARGET and
port['linkState'] == client_obj.PORT_STATE_READY
):
port['nsp'] = self.build_nsp(port['portPos'])
target_ports.append(port)
return target_ports
def get_active_fc_target_ports(self):
ports = self.get_active_target_ports()
def get_active_fc_target_ports(self, remote_client=None):
ports = self.get_active_target_ports(remote_client)
if remote_client:
client_obj = remote_client
else:
client_obj = self.client
fc_ports = []
for port in ports:
if port['protocol'] == self.client.PORT_PROTO_FC:
if port['protocol'] == client_obj.PORT_PROTO_FC:
fc_ports.append(port)
return fc_ports
def get_active_iscsi_target_ports(self):
ports = self.get_active_target_ports()
def get_active_iscsi_target_ports(self, remote_client=None):
ports = self.get_active_target_ports(remote_client)
if remote_client:
client_obj = remote_client
else:
client_obj = self.client
iscsi_ports = []
for port in ports:
if port['protocol'] == self.client.PORT_PROTO_ISCSI:
if port['protocol'] == client_obj.PORT_PROTO_ISCSI:
iscsi_ports.append(port)
return iscsi_ports
@ -1663,9 +1686,14 @@ class HPE3PARCommon(object):
'license': license_to_check})
return False
def _get_vlun(self, volume_name, hostname, lun_id=None, nsp=None):
def _get_vlun(self, volume_name, hostname, lun_id=None, nsp=None,
remote_client=None):
"""find a VLUN on a 3PAR host."""
vluns = self.client.getHostVLUNs(hostname)
if remote_client:
vluns = remote_client.getHostVLUNs(hostname)
else:
vluns = self.client.getHostVLUNs(hostname)
found_vlun = None
for vlun in vluns:
if volume_name in vlun['volumeName']:
@ -1688,26 +1716,29 @@ class HPE3PARCommon(object):
{'name': volume_name, 'host': hostname})
return found_vlun
def create_vlun(self, volume, host, nsp=None, lun_id=None):
def create_vlun(self, volume, host, nsp=None, lun_id=None,
remote_client=None):
"""Create a VLUN.
In order to export a volume on a 3PAR box, we have to create a VLUN.
"""
volume_name = self._get_3par_vol_name(volume['id'])
vlun_info = self._create_3par_vlun(volume_name, host['name'], nsp,
lun_id=lun_id)
lun_id=lun_id,
remote_client=remote_client)
return self._get_vlun(volume_name,
host['name'],
vlun_info['lun_id'],
nsp)
nsp,
remote_client)
def delete_vlun(self, volume, hostname, wwn=None, iqn=None):
def _delete_vlun(self, client_obj, volume, hostname, wwn=None, iqn=None):
volume_name = self._get_3par_vol_name(volume['id'])
if hostname:
vluns = self.client.getHostVLUNs(hostname)
vluns = client_obj.getHostVLUNs(hostname)
else:
# In case of 'force detach', hostname is None
vluns = self.client.getVLUNs()['members']
vluns = client_obj.getVLUNs()['members']
# When deleteing VLUNs, you simply need to remove the template VLUN
# and any active VLUNs will be automatically removed. The template
@ -1732,19 +1763,19 @@ class HPE3PARCommon(object):
if hostname is None:
hostname = vlun.get('hostname')
if 'portPos' in vlun:
self.client.deleteVLUN(volume_name, vlun['lun'],
hostname=hostname,
port=vlun['portPos'])
client_obj.deleteVLUN(volume_name, vlun['lun'],
hostname=hostname,
port=vlun['portPos'])
else:
self.client.deleteVLUN(volume_name, vlun['lun'],
hostname=hostname)
client_obj.deleteVLUN(volume_name, vlun['lun'],
hostname=hostname)
# Determine if there are other volumes attached to the host.
# This will determine whether we should try removing host from host set
# and deleting the host.
vluns = []
try:
vluns = self.client.getHostVLUNs(hostname)
vluns = client_obj.getHostVLUNs(hostname)
except hpeexceptions.HTTPNotFound:
LOG.debug("All VLUNs removed from host %s", hostname)
@ -1772,7 +1803,7 @@ class HPE3PARCommon(object):
try:
# TODO(sonivi): since multiattach is not supported for now,
# delete only single host, if its not exported to volume.
self._delete_3par_host(hostname)
self._delete_3par_host(hostname, client_obj)
except Exception as ex:
# Any exception down here is only logged. The vlun is deleted.
@ -1790,13 +1821,13 @@ class HPE3PARCommon(object):
'reason': ex.get_description()})
elif modify_host:
if wwn is not None:
mod_request = {'pathOperation': self.client.HOST_EDIT_REMOVE,
mod_request = {'pathOperation': client_obj.HOST_EDIT_REMOVE,
'FCWWNs': wwn}
else:
mod_request = {'pathOperation': self.client.HOST_EDIT_REMOVE,
mod_request = {'pathOperation': client_obj.HOST_EDIT_REMOVE,
'iSCSINames': iqn}
try:
self.client.modifyHost(hostname, mod_request)
client_obj.modifyHost(hostname, mod_request)
except Exception as ex:
LOG.info("3PAR vlun for volume '%(name)s' was deleted, "
"but the host '%(host)s' was not Modified "
@ -1804,6 +1835,12 @@ class HPE3PARCommon(object):
{'name': volume_name, 'host': hostname,
'reason': ex.get_description()})
def delete_vlun(self, volume, hostname, wwn=None, iqn=None,
remote_client=None):
self._delete_vlun(self.client, volume, hostname, wwn, iqn)
if remote_client:
self._delete_vlun(remote_client, volume, hostname, wwn, iqn)
def _get_volume_type(self, type_id):
ctxt = context.get_admin_context()
return volume_types.get_volume_type(ctxt, type_id)
@ -3031,7 +3068,8 @@ class HPE3PARCommon(object):
if wwn.upper() == fc['wwn'].upper():
return host['name']
def terminate_connection(self, volume, hostname, wwn=None, iqn=None):
def terminate_connection(self, volume, hostname, wwn=None, iqn=None,
remote_client=None):
"""Driver entry point to detach a volume from an instance."""
if volume.multiattach:
attachment_list = volume.volume_attachment
@ -3062,7 +3100,8 @@ class HPE3PARCommon(object):
hostname = hosts['members'][0]['name']
try:
self.delete_vlun(volume, hostname, wwn=wwn, iqn=iqn)
self.delete_vlun(volume, hostname, wwn=wwn, iqn=iqn,
remote_client=remote_client)
return
except hpeexceptions.HTTPNotFound as e:
if 'host does not exist' in e.get_description():
@ -3099,7 +3138,8 @@ class HPE3PARCommon(object):
raise
# try again with name retrieved from 3par
self.delete_vlun(volume, hostname, wwn=wwn, iqn=iqn)
self.delete_vlun(volume, hostname, wwn=wwn, iqn=iqn,
remote_client=remote_client)
def build_nsp(self, portPos):
return '%s:%s:%s' % (portPos['node'],
@ -3483,7 +3523,7 @@ class HPE3PARCommon(object):
LOG.info("Volume %(volume)s succesfully reverted to %(snap)s.",
{'volume': volume_name, 'snap': snapshot_name})
def find_existing_vlun(self, volume, host):
def find_existing_vlun(self, volume, host, remote_client=None):
"""Finds an existing VLUN for a volume on a host.
Returns an existing VLUN's information. If no existing VLUN is found,
@ -3495,7 +3535,10 @@ class HPE3PARCommon(object):
existing_vlun = None
try:
vol_name = self._get_3par_vol_name(volume['id'])
host_vluns = self.client.getHostVLUNs(host['name'])
if remote_client:
host_vluns = remote_client.getHostVLUNs(host['name'])
else:
host_vluns = self.client.getHostVLUNs(host['name'])
# The first existing VLUN found will be returned.
for vlun in host_vluns:
@ -3510,11 +3553,14 @@ class HPE3PARCommon(object):
'vol': vol_name})
return existing_vlun
def find_existing_vluns(self, volume, host):
def find_existing_vluns(self, volume, host, remote_client=None):
existing_vluns = []
try:
vol_name = self._get_3par_vol_name(volume['id'])
host_vluns = self.client.getHostVLUNs(host['name'])
if remote_client:
host_vluns = remote_client.getHostVLUNs(host['name'])
else:
host_vluns = self.client.getHostVLUNs(host['name'])
for vlun in host_vluns:
if vlun['volumeName'] == vol_name:

View File

@ -113,10 +113,11 @@ class HPE3PARFCDriver(hpebasedriver.HPE3PARDriverBase):
4.0.5 - Set proper backend on subsequent operation, after group
failover. bug #1773069
4.0.6 - Set NSP for single path attachments. Bug #1809249
4.0.7 - Added Peer Persistence feature
"""
VERSION = "4.0.5"
VERSION = "4.0.7"
# The name of the CI wiki page.
CI_WIKI_NAME = "HPE_Storage_CI"
@ -126,6 +127,43 @@ class HPE3PARFCDriver(hpebasedriver.HPE3PARDriverBase):
self.lookup_service = fczm_utils.create_lookup_service()
self.protocol = 'FC'
def _initialize_connection_common(self, volume, connector, common, host,
target_wwns, init_targ_map, numPaths,
remote_client=None):
# check if a VLUN already exists for this host
existing_vlun = common.find_existing_vlun(volume, host, remote_client)
vlun = None
if existing_vlun is None:
# now that we have a host, create the VLUN
if self.lookup_service and numPaths == 1:
nsp = None
active_fc_port_list = (
common.get_active_fc_target_ports(remote_client))
for port in active_fc_port_list:
if port['portWWN'].lower() == target_wwns[0].lower():
nsp = port['nsp']
break
vlun = common.create_vlun(volume, host, nsp, None,
remote_client)
else:
vlun = common.create_vlun(volume, host, None, None,
remote_client)
else:
vlun = existing_vlun
info_backend = {'driver_volume_type': 'fibre_channel',
'data': {'target_lun': vlun['lun'],
'target_discovered': True,
'target_wwn': target_wwns,
'initiator_target_map': init_targ_map}}
encryption_key_id = volume.get('encryption_key_id')
info_backend['data']['encrypted'] = encryption_key_id is not None
fczm_utils.add_fc_zone(info_backend)
return info_backend
@utils.trace
@coordination.synchronized('3par-{volume.id}')
def initialize_connection(self, volume, connector):
@ -167,16 +205,20 @@ class HPE3PARFCDriver(hpebasedriver.HPE3PARDriverBase):
* Create a VLUN for that HOST with the volume we want to export.
"""
LOG.debug("volume id: %(volume_id)s",
{'volume_id': volume['id']})
array_id = self.get_volume_replication_driver_data(volume)
common = self._login(array_id=array_id)
try:
# we have to make sure we have a host
host = self._create_host(common, volume, connector)
host, cpg = self._create_host(common, volume, connector)
target_wwns, init_targ_map, numPaths = (
self._build_initiator_target_map(common, connector))
multipath = connector.get('multipath')
LOG.debug("multipath: %s", multipath)
LOG.debug("multipath: %(multipath)s",
{'multipath': multipath})
user_target = None
if not multipath:
user_target = self._get_user_target(common)
@ -188,34 +230,64 @@ class HPE3PARFCDriver(hpebasedriver.HPE3PARDriverBase):
target_wwns = [user_target]
init_targ_map[initiator] = [user_target]
# check if a VLUN already exists for this host
existing_vlun = common.find_existing_vlun(volume, host)
info = self._initialize_connection_common(
volume, connector, common, host,
target_wwns, init_targ_map, numPaths)
vlun = None
if existing_vlun is None:
# now that we have a host, create the VLUN
if self.lookup_service is not None and numPaths == 1:
nsp = None
active_fc_port_list = common.get_active_fc_target_ports()
for port in active_fc_port_list:
if port['portWWN'].lower() == target_wwns[0].lower():
nsp = port['nsp']
break
vlun = common.create_vlun(volume, host, nsp)
if not multipath:
return info
if volume.get('replication_status') != 'enabled':
return info
LOG.debug('This is a replication setup')
remote_target = common._replication_targets[0]
replication_mode = remote_target['replication_mode']
quorum_witness_ip = remote_target.get('quorum_witness_ip')
if replication_mode == 1:
LOG.debug('replication_mode is sync')
if quorum_witness_ip:
LOG.debug('quorum_witness_ip is present')
LOG.debug('Peer Persistence has been configured')
else:
vlun = common.create_vlun(volume, host)
LOG.debug('Since quorum_witness_ip is absent, '
'considering this as Active/Passive '
'replication')
return info
else:
vlun = existing_vlun
LOG.debug('Active/Passive replication has been '
'configured')
return info
# Peer Persistence has been configured
remote_client = common._create_replication_client(remote_target)
host, cpg = self._create_host(
common, volume, connector,
remote_target, cpg, remote_client)
target_wwns, init_targ_map, numPaths = (
self._build_initiator_target_map(
common, connector, remote_client))
info_peer = self._initialize_connection_common(
volume, connector, common, host,
target_wwns, init_targ_map, numPaths,
remote_client)
common._destroy_replication_client(remote_client)
info = {'driver_volume_type': 'fibre_channel',
'data': {'target_lun': vlun['lun'],
'data': {'encrypted': info['data']['encrypted'],
'target_lun': info['data']['target_lun'],
'target_discovered': True,
'target_wwn': target_wwns,
'initiator_target_map': init_targ_map}}
'target_wwn': info['data']['target_wwn'] +
info_peer['data']['target_wwn'],
'initiator_target_map': self.merge_dicts(
info['data']['initiator_target_map'],
info_peer['data']['initiator_target_map'])}}
encryption_key_id = volume.get('encryption_key_id', None)
info['data']['encrypted'] = encryption_key_id is not None
fczm_utils.add_fc_zone(info)
return info
finally:
self._logout(common)
@ -228,6 +300,39 @@ class HPE3PARFCDriver(hpebasedriver.HPE3PARDriverBase):
common = self._login(array_id=array_id)
try:
is_force_detach = connector is None
remote_client = None
multipath = False
if connector:
multipath = connector.get('multipath')
LOG.debug("multipath: %(multipath)s",
{'multipath': multipath})
if multipath:
if volume.get('replication_status') == 'enabled':
LOG.debug('This is a replication setup')
remote_target = common._replication_targets[0]
replication_mode = remote_target['replication_mode']
quorum_witness_ip = (
remote_target.get('quorum_witness_ip'))
if replication_mode == 1:
LOG.debug('replication_mode is sync')
if quorum_witness_ip:
LOG.debug('quorum_witness_ip is present')
LOG.debug('Peer Persistence has been configured')
else:
LOG.debug('Since quorum_witness_ip is absent, '
'considering this as Active/Passive '
'replication')
else:
LOG.debug('Active/Passive replication has been '
'configured')
if replication_mode == 1 and quorum_witness_ip:
remote_client = (
common._create_replication_client(remote_target))
if is_force_detach:
common.terminate_connection(volume, None, None)
# TODO(sonivi): remove zones, if not required
@ -236,7 +341,8 @@ class HPE3PARFCDriver(hpebasedriver.HPE3PARDriverBase):
else:
hostname = common._safe_hostname(connector['host'])
common.terminate_connection(volume, hostname,
wwn=connector['wwpns'])
wwn=connector['wwpns'],
remote_client=remote_client)
zone_remove = True
try:
@ -259,21 +365,61 @@ class HPE3PARFCDriver(hpebasedriver.HPE3PARDriverBase):
if zone_remove:
LOG.info("Need to remove FC Zone, building initiator "
"target map")
target_wwns, init_targ_map, _numPaths = \
self._build_initiator_target_map(common, connector)
target_wwns, init_targ_map, _numPaths = (
self._build_initiator_target_map(common, connector))
info['data'] = {'target_wwn': target_wwns,
'initiator_target_map': init_targ_map}
fczm_utils.remove_fc_zone(info)
if remote_client:
if zone_remove:
try:
vluns = remote_client.getHostVLUNs(hostname)
except hpeexceptions.HTTPNotFound:
# No more exports for this host.
pass
else:
# Vlun exists, so check for wwpn entry.
for wwpn in connector.get('wwpns'):
for vlun in vluns:
if (vlun.get('active') and
vlun.get('remoteName') == wwpn.upper()):
zone_remove = False
break
info_peer = {'driver_volume_type': 'fibre_channel',
'data': {}}
if zone_remove:
LOG.info("Need to remove FC Zone, building initiator "
"target map")
target_wwns, init_targ_map, _numPaths = (
self._build_initiator_target_map(common, connector,
remote_client))
info_peer['data'] = {'target_wwn': target_wwns,
'initiator_target_map': init_targ_map}
fczm_utils.remove_fc_zone(info_peer)
info = (
{'driver_volume_type': 'fibre_channel',
'data': {'target_wwn': info['data']['target_wwn'] +
info_peer['data']['target_wwn'],
'initiator_target_map': self.merge_dicts(
info['data']['initiator_target_map'],
info_peer['data']['initiator_target_map'])}})
return info
finally:
self._logout(common)
def _build_initiator_target_map(self, common, connector):
def _build_initiator_target_map(self, common, connector,
remote_client=None):
"""Build the target_wwns and the initiator target map."""
fc_ports = common.get_active_fc_target_ports()
fc_ports = common.get_active_fc_target_ports(remote_client)
all_target_wwns = []
target_wwns = []
init_targ_map = {}
@ -311,7 +457,7 @@ class HPE3PARFCDriver(hpebasedriver.HPE3PARDriverBase):
return target_wwns, init_targ_map, numPaths
def _create_3par_fibrechan_host(self, common, hostname, wwns,
domain, persona_id):
domain, persona_id, remote_client=None):
"""Create a 3PAR host.
Create a 3PAR host, if there is already a host on the 3par using
@ -320,7 +466,13 @@ class HPE3PARFCDriver(hpebasedriver.HPE3PARDriverBase):
"""
# first search for an existing host
host_found = None
hosts = common.client.queryHost(wwns=wwns)
if remote_client:
client_obj = remote_client
else:
client_obj = common.client
hosts = client_obj.queryHost(wwns=wwns)
if hosts and hosts['members'] and 'name' in hosts['members'][0]:
host_found = hosts['members'][0]['name']
@ -330,9 +482,9 @@ class HPE3PARFCDriver(hpebasedriver.HPE3PARDriverBase):
else:
persona_id = int(persona_id)
try:
common.client.createHost(hostname, FCWwns=wwns,
optional={'domain': domain,
'persona': persona_id})
client_obj.createHost(hostname, FCWwns=wwns,
optional={'domain': domain,
'persona': persona_id})
except hpeexceptions.HTTPConflict as path_conflict:
msg = "Create FC host caught HTTP conflict code: %s"
LOG.exception(msg, path_conflict.get_code())
@ -340,7 +492,7 @@ class HPE3PARFCDriver(hpebasedriver.HPE3PARDriverBase):
if path_conflict.get_code() is EXISTENT_PATH:
# Handle exception : EXISTENT_PATH - host WWN/iSCSI
# name already used by another host
hosts = common.client.queryHost(wwns=wwns)
hosts = client_obj.queryHost(wwns=wwns)
if hosts and hosts['members'] and (
'name' in hosts['members'][0]):
hostname = hosts['members'][0]['name']
@ -353,11 +505,17 @@ class HPE3PARFCDriver(hpebasedriver.HPE3PARDriverBase):
ctxt.reraise = True
return hostname
def _modify_3par_fibrechan_host(self, common, hostname, wwn):
mod_request = {'pathOperation': common.client.HOST_EDIT_ADD,
def _modify_3par_fibrechan_host(self, common, hostname, wwn,
remote_client):
if remote_client:
client_obj = remote_client
else:
client_obj = common.client
mod_request = {'pathOperation': client_obj.HOST_EDIT_ADD,
'FCWWNs': wwn}
try:
common.client.modifyHost(hostname, mod_request)
client_obj.modifyHost(hostname, mod_request)
except hpeexceptions.HTTPConflict as path_conflict:
msg = ("Modify FC Host %(hostname)s caught "
"HTTP conflict code: %(code)s")
@ -365,21 +523,34 @@ class HPE3PARFCDriver(hpebasedriver.HPE3PARDriverBase):
{'hostname': hostname,
'code': path_conflict.get_code()})
def _create_host(self, common, volume, connector):
def _create_host(self, common, volume, connector,
remote_target=None, src_cpg=None, remote_client=None):
"""Creates or modifies existing 3PAR host."""
host = None
domain = None
hostname = common._safe_hostname(connector['host'])
cpg = common.get_cpg(volume, allowSnap=True)
domain = common.get_domain(cpg)
if remote_target:
cpg = common._get_cpg_from_cpg_map(
remote_target['cpg_map'], src_cpg)
cpg_obj = remote_client.getCPG(cpg)
if 'domain' in cpg_obj:
domain = cpg_obj['domain']
else:
cpg = common.get_cpg(volume, allowSnap=True)
domain = common.get_domain(cpg)
if not connector.get('multipath'):
connector['wwpns'] = connector['wwpns'][:1]
try:
host = common._get_3par_host(hostname)
# Check whether host with wwn of initiator present on 3par
hosts = common.client.queryHost(wwns=connector['wwpns'])
host, hostname = common._get_prioritized_host_on_3par(host,
hosts,
hostname)
if remote_target:
host = remote_client.getHost(hostname)
else:
host = common._get_3par_host(hostname)
# Check whether host with wwn of initiator present on 3par
hosts = common.client.queryHost(wwns=connector['wwpns'])
host, hostname = (
common._get_prioritized_host_on_3par(
host, hosts, hostname))
except hpeexceptions.HTTPNotFound:
# get persona from the volume type extra specs
persona_id = common.get_persona_type(volume)
@ -388,13 +559,19 @@ class HPE3PARFCDriver(hpebasedriver.HPE3PARDriverBase):
hostname,
connector['wwpns'],
domain,
persona_id)
host = common._get_3par_host(hostname)
return host
persona_id,
remote_client)
if remote_target:
host = remote_client.getHost(hostname)
else:
host = common._get_3par_host(hostname)
return host, cpg
else:
return self._add_new_wwn_to_host(common, host, connector['wwpns'])
host = self._add_new_wwn_to_host(
common, host, connector['wwpns'], remote_client)
return host, cpg
def _add_new_wwn_to_host(self, common, host, wwns):
def _add_new_wwn_to_host(self, common, host, wwns, remote_client=None):
"""Add wwns to a host if one or more don't exist.
Identify if argument wwns contains any world wide names
@ -419,8 +596,12 @@ class HPE3PARFCDriver(hpebasedriver.HPE3PARDriverBase):
# if any wwns found that were not in host list,
# add them to the host
if (len(new_wwns) > 0):
self._modify_3par_fibrechan_host(common, host['name'], new_wwns)
host = common._get_3par_host(host['name'])
self._modify_3par_fibrechan_host(
common, host['name'], new_wwns, remote_client)
if remote_client:
host = remote_client.getHost(host['name'])
else:
host = common._get_3par_host(host['name'])
return host
def _get_user_target(self, common):
@ -444,3 +625,8 @@ class HPE3PARFCDriver(hpebasedriver.HPE3PARDriverBase):
"%(nsp)s", {'nsp': target_nsp})
return target_wwn
def merge_dicts(self, dict_1, dict_2):
keys = set(dict_1).union(dict_2)
no = []
return {k: (dict_1.get(k, no) + dict_2.get(k, no)) for k in keys}

View File

@ -126,10 +126,11 @@ class HPE3PARISCSIDriver(hpebasedriver.HPE3PARDriverBase):
4.0.2 - Handle force detach case. bug #1686745
4.0.3 - Set proper backend on subsequent operation, after group
failover. bug #1773069
4.0.4 - Added Peer Persistence feature
"""
VERSION = "4.0.2"
VERSION = "4.0.4"
# The name of the CI wiki page.
CI_WIKI_NAME = "HPE_Storage_CI"
@ -146,17 +147,23 @@ class HPE3PARISCSIDriver(hpebasedriver.HPE3PARDriverBase):
finally:
self._logout(common)
def initialize_iscsi_ports(self, common):
def initialize_iscsi_ports(self, common,
remote_target=None, remote_client=None):
# map iscsi_ip-> ip_port
# -> iqn
# -> nsp
iscsi_ip_list = {}
temp_iscsi_ip = {}
if remote_target:
backend_conf = remote_target
else:
backend_conf = common._client_conf
# use the 3PAR ip_addr list for iSCSI configuration
if len(common._client_conf['hpe3par_iscsi_ips']) > 0:
if len(backend_conf['hpe3par_iscsi_ips']) > 0:
# add port values to ip_addr, if necessary
for ip_addr in common._client_conf['hpe3par_iscsi_ips']:
for ip_addr in backend_conf['hpe3par_iscsi_ips']:
ip = ip_addr.split(':')
if len(ip) == 1:
temp_iscsi_ip[ip_addr] = {'ip_port': DEFAULT_ISCSI_PORT}
@ -168,15 +175,16 @@ class HPE3PARISCSIDriver(hpebasedriver.HPE3PARDriverBase):
# add the single value iscsi_ip_address option to the IP dictionary.
# This way we can see if it's a valid iSCSI IP. If it's not valid,
# we won't use it and won't bother to report it, see below
if (common._client_conf['iscsi_ip_address'] not in temp_iscsi_ip):
ip = common._client_conf['iscsi_ip_address']
ip_port = common._client_conf['iscsi_port']
temp_iscsi_ip[ip] = {'ip_port': ip_port}
if 'iscsi_ip_address' in backend_conf:
if (backend_conf['iscsi_ip_address'] not in temp_iscsi_ip):
ip = backend_conf['iscsi_ip_address']
ip_port = backend_conf['iscsi_port']
temp_iscsi_ip[ip] = {'ip_port': ip_port}
# get all the valid iSCSI ports from 3PAR
# when found, add the valid iSCSI ip, ip port, iqn and nsp
# to the iSCSI IP dictionary
iscsi_ports = common.get_active_iscsi_target_ports()
iscsi_ports = common.get_active_iscsi_target_ports(remote_client)
for port in iscsi_ports:
ip = port['IPAddr']
@ -190,8 +198,9 @@ class HPE3PARISCSIDriver(hpebasedriver.HPE3PARDriverBase):
# if the single value iscsi_ip_address option is still in the
# temp dictionary it's because it defaults to $my_ip which doesn't
# make sense in this context. So, if present, remove it and move on.
if common._client_conf['iscsi_ip_address'] in temp_iscsi_ip:
del temp_iscsi_ip[common._client_conf['iscsi_ip_address']]
if 'iscsi_ip_address' in backend_conf:
if backend_conf['iscsi_ip_address'] in temp_iscsi_ip:
del temp_iscsi_ip[backend_conf['iscsi_ip_address']]
# lets see if there are invalid iSCSI IPs left in the temp dict
if len(temp_iscsi_ip) > 0:
@ -204,7 +213,59 @@ class HPE3PARISCSIDriver(hpebasedriver.HPE3PARDriverBase):
msg = _('At least one valid iSCSI IP address must be set.')
LOG.error(msg)
raise exception.InvalidInput(reason=msg)
self.iscsi_ips[common._client_conf['hpe3par_api_url']] = iscsi_ip_list
if remote_target:
self.iscsi_ips[remote_target['hpe3par_api_url']] = iscsi_ip_list
else:
self.iscsi_ips[common._client_conf['hpe3par_api_url']] = (
iscsi_ip_list)
def _initialize_connection_common(self, volume, connector, common,
host, iscsi_ips, ready_ports,
target_portals, target_iqns, target_luns,
remote_client=None):
# Target portal ips are defined in cinder.conf.
target_portal_ips = iscsi_ips.keys()
# Collect all existing VLUNs for this volume/host combination.
existing_vluns = common.find_existing_vluns(volume, host,
remote_client)
# Cycle through each ready iSCSI port and determine if a new
# VLUN should be created or an existing one used.
lun_id = None
for port in ready_ports:
iscsi_ip = port['IPAddr']
if iscsi_ip in target_portal_ips:
vlun = None
# check for an already existing VLUN matching the
# nsp for this iSCSI IP. If one is found, use it
# instead of creating a new VLUN.
for v in existing_vluns:
portPos = common.build_portPos(
iscsi_ips[iscsi_ip]['nsp'])
if v['portPos'] == portPos:
vlun = v
break
else:
vlun = common.create_vlun(
volume, host, iscsi_ips[iscsi_ip]['nsp'],
lun_id=lun_id, remote_client=remote_client)
# We want to use the same LUN ID for every port
if lun_id is None:
lun_id = vlun['lun']
iscsi_ip_port = "%s:%s" % (
iscsi_ip, iscsi_ips[iscsi_ip]['ip_port'])
target_portals.append(iscsi_ip_port)
target_iqns.append(port['iSCSIName'])
target_luns.append(vlun['lun'])
else:
LOG.warning("iSCSI IP: '%s' was not found in "
"hpe3par_iscsi_ips list defined in "
"cinder.conf.", iscsi_ip)
@utils.trace
@coordination.synchronized('3par-{volume.id}')
@ -236,6 +297,8 @@ class HPE3PARISCSIDriver(hpebasedriver.HPE3PARDriverBase):
* Create a host on the 3par
* create vlun on the 3par
"""
LOG.debug("volume id: %(volume_id)s",
{'volume_id': volume['id']})
array_id = self.get_volume_replication_driver_data(volume)
common = self._login(array_id=array_id)
try:
@ -249,12 +312,15 @@ class HPE3PARISCSIDriver(hpebasedriver.HPE3PARDriverBase):
iscsi_ips = self.iscsi_ips[common._client_conf['hpe3par_api_url']]
# we have to make sure we have a host
host, username, password = self._create_host(
host, username, password, cpg = self._create_host(
common,
volume,
connector)
if connector.get('multipath'):
multipath = connector.get('multipath')
LOG.debug("multipath: %(multipath)s",
{'multipath': multipath})
if multipath:
ready_ports = common.client.getiSCSIPorts(
state=common.client.PORT_STATE_READY)
@ -262,45 +328,57 @@ class HPE3PARISCSIDriver(hpebasedriver.HPE3PARDriverBase):
target_iqns = []
target_luns = []
# Target portal ips are defined in cinder.conf.
target_portal_ips = iscsi_ips.keys()
self._initialize_connection_common(
volume, connector, common,
host, iscsi_ips, ready_ports,
target_portals, target_iqns, target_luns)
# Collect all existing VLUNs for this volume/host combination.
existing_vluns = common.find_existing_vluns(volume, host)
if volume.get('replication_status') == 'enabled':
LOG.debug('This is a replication setup')
# Cycle through each ready iSCSI port and determine if a new
# VLUN should be created or an existing one used.
lun_id = None
for port in ready_ports:
iscsi_ip = port['IPAddr']
if iscsi_ip in target_portal_ips:
vlun = None
# check for an already existing VLUN matching the
# nsp for this iSCSI IP. If one is found, use it
# instead of creating a new VLUN.
for v in existing_vluns:
portPos = common.build_portPos(
iscsi_ips[iscsi_ip]['nsp'])
if v['portPos'] == portPos:
vlun = v
break
remote_target = common._replication_targets[0]
replication_mode = remote_target['replication_mode']
quorum_witness_ip = (
remote_target.get('quorum_witness_ip'))
if replication_mode == 1:
LOG.debug('replication_mode is sync')
if quorum_witness_ip:
LOG.debug('quorum_witness_ip is present')
LOG.debug('Peer Persistence has been configured')
else:
vlun = common.create_vlun(
volume, host, iscsi_ips[iscsi_ip]['nsp'],
lun_id=lun_id)
# We want to use the same LUN ID for every port
if lun_id is None:
lun_id = vlun['lun']
iscsi_ip_port = "%s:%s" % (
iscsi_ip, iscsi_ips[iscsi_ip]['ip_port'])
target_portals.append(iscsi_ip_port)
target_iqns.append(port['iSCSIName'])
target_luns.append(vlun['lun'])
LOG.debug('Since quorum_witness_ip is absent, '
'considering this as Active/Passive '
'replication')
else:
LOG.warning("iSCSI IP: '%s' was not found in "
"hpe3par_iscsi_ips list defined in "
"cinder.conf.", iscsi_ip)
LOG.debug('Active/Passive replication has been '
'configured')
if replication_mode == 1 and quorum_witness_ip:
remote_client = (
common._create_replication_client(remote_target))
self.initialize_iscsi_ports(
common, remote_target, remote_client)
remote_iscsi_ips = (
self.iscsi_ips[remote_target['hpe3par_api_url']])
# we have to make sure we have a host
host, username, password, cpg = (
self._create_host(
common, volume, connector,
remote_target, cpg, remote_client))
ready_ports = remote_client.getiSCSIPorts(
state=remote_client.PORT_STATE_READY)
self._initialize_connection_common(
volume, connector, common,
host, remote_iscsi_ips, ready_ports,
target_portals, target_iqns, target_luns,
remote_client)
common._destroy_replication_client(remote_client)
info = {'driver_volume_type': 'iscsi',
'data': {'target_portals': target_portals,
@ -373,6 +451,39 @@ class HPE3PARISCSIDriver(hpebasedriver.HPE3PARDriverBase):
common = self._login(array_id=array_id)
try:
is_force_detach = connector is None
remote_client = None
multipath = False
if connector:
multipath = connector.get('multipath')
LOG.debug("multipath: %(multipath)s",
{'multipath': multipath})
if multipath:
if volume.get('replication_status') == 'enabled':
LOG.debug('This is a replication setup')
remote_target = common._replication_targets[0]
replication_mode = remote_target['replication_mode']
quorum_witness_ip = (
remote_target.get('quorum_witness_ip'))
if replication_mode == 1:
LOG.debug('replication_mode is sync')
if quorum_witness_ip:
LOG.debug('quorum_witness_ip is present')
LOG.debug('Peer Persistence has been configured')
else:
LOG.debug('Since quorum_witness_ip is absent, '
'considering this as Active/Passive '
'replication')
else:
LOG.debug('Active/Passive replication has been '
'configured')
if replication_mode == 1 and quorum_witness_ip:
remote_client = (
common._create_replication_client(remote_target))
if is_force_detach:
common.terminate_connection(volume, None, None)
else:
@ -380,7 +491,8 @@ class HPE3PARISCSIDriver(hpebasedriver.HPE3PARDriverBase):
common.terminate_connection(
volume,
hostname,
iqn=connector['initiator'])
iqn=connector['initiator'],
remote_client=remote_client)
self._clear_chap_3par(common, volume)
finally:
self._logout(common)
@ -407,7 +519,7 @@ class HPE3PARISCSIDriver(hpebasedriver.HPE3PARDriverBase):
raise
def _create_3par_iscsi_host(self, common, hostname, iscsi_iqn, domain,
persona_id):
persona_id, remote_client=None):
"""Create a 3PAR host.
Create a 3PAR host, if there is already a host on the 3par using
@ -417,7 +529,12 @@ class HPE3PARISCSIDriver(hpebasedriver.HPE3PARDriverBase):
# first search for an existing host
host_found = None
hosts = common.client.queryHost(iqns=iscsi_iqn)
if remote_client:
client_obj = remote_client
else:
client_obj = common.client
hosts = client_obj.queryHost(iqns=iscsi_iqn)
if hosts and hosts['members'] and 'name' in hosts['members'][0]:
host_found = hosts['members'][0]['name']
@ -427,16 +544,16 @@ class HPE3PARISCSIDriver(hpebasedriver.HPE3PARDriverBase):
else:
persona_id = int(persona_id)
try:
common.client.createHost(hostname, iscsiNames=iscsi_iqn,
optional={'domain': domain,
'persona': persona_id})
client_obj.createHost(hostname, iscsiNames=iscsi_iqn,
optional={'domain': domain,
'persona': persona_id})
except hpeexceptions.HTTPConflict as path_conflict:
msg = "Create iSCSI host caught HTTP conflict code: %s"
with save_and_reraise_exception(reraise=False) as ctxt:
if path_conflict.get_code() is EXISTENT_PATH:
# Handle exception : EXISTENT_PATH - host WWN/iSCSI
# name already used by another host
hosts = common.client.queryHost(iqns=iscsi_iqn)
hosts = client_obj.queryHost(iqns=iscsi_iqn)
if hosts and hosts['members'] and (
'name' in hosts['members'][0]):
hostname = hosts['members'][0]['name']
@ -468,31 +585,45 @@ class HPE3PARISCSIDriver(hpebasedriver.HPE3PARDriverBase):
'chapSecret': password}
common.client.modifyHost(hostname, mod_request)
def _create_host(self, common, volume, connector):
def _create_host(self, common, volume, connector,
remote_target=None, src_cpg=None, remote_client=None):
"""Creates or modifies existing 3PAR host."""
# make sure we don't have the host already
host = None
domain = None
username = None
password = None
hostname = common._safe_hostname(connector['host'])
cpg = common.get_cpg(volume, allowSnap=True)
domain = common.get_domain(cpg)
# Get the CHAP secret if CHAP is enabled
if common._client_conf['hpe3par_iscsi_chap_enabled']:
vol_name = common._get_3par_vol_name(volume['id'])
username = common.client.getVolumeMetaData(
vol_name, CHAP_USER_KEY)['value']
password = common.client.getVolumeMetaData(
vol_name, CHAP_PASS_KEY)['value']
if remote_target:
cpg = common._get_cpg_from_cpg_map(
remote_target['cpg_map'], src_cpg)
cpg_obj = remote_client.getCPG(cpg)
if 'domain' in cpg_obj:
domain = cpg_obj['domain']
else:
cpg = common.get_cpg(volume, allowSnap=True)
domain = common.get_domain(cpg)
if not remote_target:
# Get the CHAP secret if CHAP is enabled
if common._client_conf['hpe3par_iscsi_chap_enabled']:
vol_name = common._get_3par_vol_name(volume['id'])
username = common.client.getVolumeMetaData(
vol_name, CHAP_USER_KEY)['value']
password = common.client.getVolumeMetaData(
vol_name, CHAP_PASS_KEY)['value']
try:
host = common._get_3par_host(hostname)
# Check whether host with iqn of initiator present on 3par
hosts = common.client.queryHost(iqns=[connector['initiator']])
host, hostname = common._get_prioritized_host_on_3par(host,
hosts,
hostname)
if remote_target:
host = remote_client.getHost(hostname)
else:
host = common._get_3par_host(hostname)
# Check whether host with iqn of initiator present on 3par
hosts = common.client.queryHost(iqns=[connector['initiator']])
host, hostname = (
common._get_prioritized_host_on_3par(
host, hosts, hostname))
except hpeexceptions.HTTPNotFound:
# get persona from the volume type extra specs
persona_id = common.get_persona_type(volume)
@ -501,22 +632,27 @@ class HPE3PARISCSIDriver(hpebasedriver.HPE3PARDriverBase):
hostname,
[connector['initiator']],
domain,
persona_id)
persona_id,
remote_client)
else:
if 'iSCSIPaths' not in host or len(host['iSCSIPaths']) < 1:
self._modify_3par_iscsi_host(
common, hostname,
connector['initiator'])
elif (not host['initiatorChapEnabled'] and
common._client_conf['hpe3par_iscsi_chap_enabled']):
LOG.warning("Host exists without CHAP credentials set and "
"has iSCSI attachments but CHAP is enabled. "
"Updating host with new CHAP credentials.")
if not remote_target:
if 'iSCSIPaths' not in host or len(host['iSCSIPaths']) < 1:
self._modify_3par_iscsi_host(
common, hostname,
connector['initiator'])
elif (not host['initiatorChapEnabled'] and
common._client_conf['hpe3par_iscsi_chap_enabled']):
LOG.warning("Host exists without CHAP credentials set and "
"has iSCSI attachments but CHAP is enabled. "
"Updating host with new CHAP credentials.")
# set/update the chap details for the host
self._set_3par_chaps(common, hostname, volume, username, password)
host = common._get_3par_host(hostname)
return host, username, password
if remote_target:
host = remote_client.getHost(hostname)
else:
# set/update the chap details for the host
self._set_3par_chaps(common, hostname, volume, username, password)
host = common._get_3par_host(hostname)
return host, username, password, cpg
def _do_export(self, common, volume, connector):
"""Gets the associated account, generates CHAP info and updates."""

View File

@ -109,6 +109,8 @@ Supported operations
* Report Backend State in Service List.
* Peer Persistence.
Volume type support for both HPE 3PAR drivers includes the ability to set the
following capabilities in the OpenStack Block Storage API
``cinder.api.contrib.types_extra_specs`` volume type extra specs extension
@ -461,3 +463,43 @@ Note: If above mentioned option (nsp) is not specified in cinder.conf,
then the original flow is executed i.e first target is picked and
bootable volume creation may fail.
Peer Persistence support
~~~~~~~~~~~~~~~~~~~~~~~~
Given 3PAR backend configured with replication setup, currently only
Active/Passive replication is supported by 3PAR in OpenStack. When
failover happens, nova does not support volume force-detach (from
dead primary backend) / re-attach to secondary backend. Storage
engineer's manual intervention is required.
To overcome above scenario, support for Peer Persistence is added.
Given a system with Peer Persistence configured and replicated volume
is created. When this volume is attached to an instance, vlun is
created automatically in secondary backend, in addition to primary
backend. So that when a failover happens, it is seamless.
For Peer Persistence support, perform following steps:
1] enable multipath
2] set replication mode as "sync"
3] configure a quorum witness server
Specify ip address of quorum witness server in ``/etc/cinder/cinder.conf``
[within backend section] as given below:
.. code-block:: console
[3pariscsirep]
hpe3par_api_url = http://10.50.3.7:8008/api/v1
hpe3par_username = <user_name>
hpe3par_password = <password>
...
<other parameters>
...
replication_device = backend_id:CSIM-EOS12_1611702,
replication_mode:sync,
quorum_witness_ip:10.50.3.192,
hpe3par_api_url:http://10.50.3.22:8008/api/v1,
...
<other parameters>
...

View File

@ -896,7 +896,7 @@ driver.dell_emc_vnx=missing
driver.dell_emc_vxflexos=missing
driver.dell_emc_xtremio=missing
driver.fujitsu_eternus=missing
driver.hpe_3par=missing
driver.hpe_3par=complete
driver.hpe_lefthand=missing
driver.hpe_msa=missing
driver.huawei_t_v1=missing

View File

@ -0,0 +1,5 @@
---
features:
- |
Added Peer Persistence support in HPE 3PAR cinder driver.