3PAR: Implement v2 replication (unmanaged)
This patch implements the unmanaged side of v2 replication in the HPE 3PAR driver. Both sync and periodic replication modes are supported. Each replication_device entry should have a replication_mode value set to sync|periodic. A volume type extra_spec value of replication:mode should also be set. If replication:mode is periodic, replication:sync_period should be set as well. Which replication_device entry(s) are used is determined by the value of replication:mode set for each volume type. cinder.conf should have the replication config group: [3parfcrep] hpe3par_api_url = http://10.10.10.10:8008/api/v1 hpe3par_username = user hpe3par_password = pass hpe3par_debug = False san_ip = 10.10.10.10 san_login = user san_password = pass volume_backend_name = 3parfcrep hpe3par_cpg = REMOTE_COPY_CPG2,REMOTE_COPY_CPG volume_driver = cinder.volume.drivers.hpe.hpe_3par_fc.HPE3PARFCDriver replication_device = target_device_id:eos7, replication_mode:periodic, cpg_map:REMOTE_COPY_CPG2:REMOTE_COPY_DEST2, hpe3par_api_url:http://11.11.11.11:8008/api/v1, hpe3par_username:user, hpe3par_password:pass, san_ip:11.11.11.11, san_login:user, san_password:pass If we are working with iSCSI, the unmanaged replication device needs to contain entries for the hpe3par_iscsi_ips as such: [3pariscsirep] hpe3par_api_url = https://10.10.10.10:8080/api/v1 hpe3par_username = user hpe3par_password = pass hpe3par_debug = False hpe3par_iscsi_ips = 10.50.50.50,10.50.50.51 san_ip = 10.10.10.10 san_login = user san_password = pass volume_backend_name = 3pariscsirep hpe3par_cpg = REMOTE_COPY_CPG2 iscsi_ip_address = 10.50.50.50 volume_driver = cinder.volume.drivers.hpe.hpe_3par_iscsi.HPE3PARISCSIDriver replication_device = replication_mode:periodic, target_device_id:eos16, cpg_map:REMOTE_COPY_CPG2:REMOTE_COPY_DEST2, hpe3par_api_url:https://11.11.11.11:8080/api/v1, hpe3par_username:user, hpe3par_password:pass, san_ip:11.11.11.11, san_login:user, san_password:pass, hpe3par_iscsi_ips:11.51.51.100 11.51.51.101, iscsi_ip_address:11.51.51.100 Change-Id: I92bbe919c177e8242de8affad62f36a6edf7b0d7 Implements: blueprint hp-3par-v2-replication DocImpact
This commit is contained in:
@@ -172,6 +172,19 @@ class HPE3PARBaseDriver(object):
|
||||
|
||||
list_rep_targets = [{'target_device_id': 'target'}]
|
||||
|
||||
replication_devs_unmgd = [{'target_device_id': 'target',
|
||||
'cpg_map': HPE3PAR_CPG_MAP,
|
||||
'hpe3par_api_url': 'https://1.1.1.1/api/v1',
|
||||
'hpe3par_username': HPE3PAR_USER_NAME,
|
||||
'hpe3par_password': HPE3PAR_USER_PASS,
|
||||
'san_ip': HPE3PAR_SAN_IP,
|
||||
'san_login': HPE3PAR_USER_NAME,
|
||||
'san_password': HPE3PAR_USER_PASS,
|
||||
'san_ssh_port': HPE3PAR_SAN_SSH_PORT,
|
||||
'ssh_conn_timeout': HPE3PAR_SAN_SSH_CON_TIMEOUT,
|
||||
'san_private_key': HPE3PAR_SAN_SSH_PRIVATE,
|
||||
'managed_backend_name': None}]
|
||||
|
||||
volume_encrypted = {'name': VOLUME_NAME,
|
||||
'id': VOLUME_ID,
|
||||
'display_name': 'Foo Volume',
|
||||
@@ -223,7 +236,8 @@ class HPE3PARBaseDriver(object):
|
||||
'progress': '0%',
|
||||
'volume_size': 2,
|
||||
'display_name': 'fakesnap',
|
||||
'display_description': FAKE_DESC}
|
||||
'display_description': FAKE_DESC,
|
||||
'volume': volume}
|
||||
|
||||
wwn = ["123456789012345", "123456789054321"]
|
||||
|
||||
@@ -1110,6 +1124,179 @@ class HPE3PARBaseDriver(object):
|
||||
'provider_location': self.CLIENT_ID},
|
||||
return_model)
|
||||
|
||||
@mock.patch('hpe3parclient.version', "4.0.2")
|
||||
@mock.patch.object(volume_types, 'get_volume_type')
|
||||
def test_create_volume_replicated_unmanaged_periodic(self,
|
||||
_mock_volume_types):
|
||||
# setup_mock_client drive with default configuration
|
||||
# and return the mock HTTP 3PAR client
|
||||
conf = self.setup_configuration()
|
||||
self.replication_devs_unmgd[0]['replication_mode'] = 'periodic'
|
||||
conf.replication_device = self.replication_devs_unmgd
|
||||
mock_client = self.setup_driver(config=conf)
|
||||
mock_client.getStorageSystemInfo.return_value = {'id': self.CLIENT_ID}
|
||||
mock_client.getRemoteCopyGroup.side_effect = hpeexceptions.HTTPNotFound
|
||||
mock_client.getCPG.return_value = {'domain': None}
|
||||
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': {
|
||||
'cpg': HPE3PAR_CPG,
|
||||
'snap_cpg': HPE3PAR_CPG_SNAP,
|
||||
'replication_enabled': '<is> True',
|
||||
'replication:mode': 'periodic',
|
||||
'replication:sync_period': '900',
|
||||
'volume_type': self.volume_type_replicated}}
|
||||
|
||||
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
|
||||
|
||||
return_model = self.driver.create_volume(self.volume_replicated)
|
||||
comment = Comment({
|
||||
"volume_type_name": "replicated",
|
||||
"display_name": "Foo Volume",
|
||||
"name": "volume-d03338a9-9115-48a3-8dfc-35cdfcdc15a7",
|
||||
"volume_type_id": "be9181f1-4040-46f2-8298-e7532f2bf9db",
|
||||
"volume_id": "d03338a9-9115-48a3-8dfc-35cdfcdc15a7",
|
||||
"qos": {},
|
||||
"type": "OpenStack"})
|
||||
|
||||
target_device_id = self.replication_targets[0]['target_device_id']
|
||||
expected = [
|
||||
mock.call.getCPG(HPE3PAR_CPG),
|
||||
mock.call.createVolume(
|
||||
self.VOLUME_3PAR_NAME,
|
||||
HPE3PAR_CPG,
|
||||
2048, {
|
||||
'comment': comment,
|
||||
'tpvv': True,
|
||||
'tdvv': False,
|
||||
'snapCPG': HPE3PAR_CPG_SNAP}),
|
||||
mock.call.getRemoteCopyGroup(self.RCG_3PAR_NAME),
|
||||
mock.call.getCPG(HPE3PAR_CPG),
|
||||
mock.call.getCPG(HPE3PAR_CPG),
|
||||
mock.call.createRemoteCopyGroup(
|
||||
self.RCG_3PAR_NAME,
|
||||
[{'userCPG': HPE3PAR_CPG_REMOTE,
|
||||
'targetName': target_device_id,
|
||||
'mode': PERIODIC_MODE,
|
||||
'snapCPG': HPE3PAR_CPG_REMOTE}],
|
||||
{'localUserCPG': HPE3PAR_CPG,
|
||||
'localSnapCPG': HPE3PAR_CPG_SNAP}),
|
||||
mock.call.addVolumeToRemoteCopyGroup(
|
||||
self.RCG_3PAR_NAME,
|
||||
self.VOLUME_3PAR_NAME,
|
||||
[{'secVolumeName': self.VOLUME_3PAR_NAME,
|
||||
'targetName': target_device_id}],
|
||||
optional={'volumeAutoCreation': True}),
|
||||
mock.call.modifyRemoteCopyGroup(
|
||||
self.RCG_3PAR_NAME,
|
||||
{'targets': [{'syncPeriod': SYNC_PERIOD,
|
||||
'targetName': target_device_id}]}),
|
||||
mock.call.startRemoteCopy(self.RCG_3PAR_NAME)]
|
||||
mock_client.assert_has_calls(
|
||||
self.get_id_login +
|
||||
self.standard_logout +
|
||||
self.standard_login +
|
||||
expected +
|
||||
self.standard_logout)
|
||||
self.assertEqual({'replication_status': 'enabled',
|
||||
'provider_location': self.CLIENT_ID},
|
||||
return_model)
|
||||
|
||||
@mock.patch('hpe3parclient.version', "4.0.2")
|
||||
@mock.patch.object(volume_types, 'get_volume_type')
|
||||
def test_create_volume_replicated_unmanaged_sync(self,
|
||||
_mock_volume_types):
|
||||
# setup_mock_client drive with default configuration
|
||||
# and return the mock HTTP 3PAR client
|
||||
conf = self.setup_configuration()
|
||||
self.replication_devs_unmgd[0]['replication_mode'] = 'sync'
|
||||
conf.replication_device = self.replication_devs_unmgd
|
||||
mock_client = self.setup_driver(config=conf)
|
||||
mock_client.getStorageSystemInfo.return_value = {'id': self.CLIENT_ID}
|
||||
mock_client.getRemoteCopyGroup.side_effect = hpeexceptions.HTTPNotFound
|
||||
mock_client.getCPG.return_value = {'domain': None}
|
||||
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': {
|
||||
'cpg': HPE3PAR_CPG,
|
||||
'snap_cpg': HPE3PAR_CPG_SNAP,
|
||||
'replication_enabled': '<is> True',
|
||||
'replication:mode': 'sync',
|
||||
'volume_type': self.volume_type_replicated}}
|
||||
|
||||
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
|
||||
|
||||
return_model = self.driver.create_volume(self.volume_replicated)
|
||||
comment = Comment({
|
||||
"volume_type_name": "replicated",
|
||||
"display_name": "Foo Volume",
|
||||
"name": "volume-d03338a9-9115-48a3-8dfc-35cdfcdc15a7",
|
||||
"volume_type_id": "be9181f1-4040-46f2-8298-e7532f2bf9db",
|
||||
"volume_id": "d03338a9-9115-48a3-8dfc-35cdfcdc15a7",
|
||||
"qos": {},
|
||||
"type": "OpenStack"})
|
||||
|
||||
target_device_id = self.replication_targets[0]['target_device_id']
|
||||
expected = [
|
||||
mock.call.getCPG(HPE3PAR_CPG),
|
||||
mock.call.createVolume(
|
||||
self.VOLUME_3PAR_NAME,
|
||||
HPE3PAR_CPG,
|
||||
2048, {
|
||||
'comment': comment,
|
||||
'tpvv': True,
|
||||
'tdvv': False,
|
||||
'snapCPG': HPE3PAR_CPG_SNAP}),
|
||||
mock.call.getRemoteCopyGroup(self.RCG_3PAR_NAME),
|
||||
mock.call.getCPG(HPE3PAR_CPG),
|
||||
mock.call.getCPG(HPE3PAR_CPG),
|
||||
mock.call.createRemoteCopyGroup(
|
||||
self.RCG_3PAR_NAME,
|
||||
[{'userCPG': HPE3PAR_CPG_REMOTE,
|
||||
'targetName': target_device_id,
|
||||
'mode': SYNC_MODE,
|
||||
'snapCPG': HPE3PAR_CPG_REMOTE}],
|
||||
{'localUserCPG': HPE3PAR_CPG,
|
||||
'localSnapCPG': HPE3PAR_CPG_SNAP}),
|
||||
mock.call.addVolumeToRemoteCopyGroup(
|
||||
self.RCG_3PAR_NAME,
|
||||
self.VOLUME_3PAR_NAME,
|
||||
[{'secVolumeName': self.VOLUME_3PAR_NAME,
|
||||
'targetName': target_device_id}],
|
||||
optional={'volumeAutoCreation': True}),
|
||||
mock.call.startRemoteCopy(self.RCG_3PAR_NAME)]
|
||||
mock_client.assert_has_calls(
|
||||
self.get_id_login +
|
||||
self.standard_logout +
|
||||
self.standard_login +
|
||||
expected +
|
||||
self.standard_logout)
|
||||
self.assertEqual({'replication_status': 'enabled',
|
||||
'provider_location': self.CLIENT_ID},
|
||||
return_model)
|
||||
|
||||
@mock.patch.object(volume_types, 'get_volume_type')
|
||||
def test_create_volume_dedup(self, _mock_volume_types):
|
||||
# setup_mock_client drive with default configuration
|
||||
@@ -4274,6 +4461,89 @@ class HPE3PARBaseDriver(object):
|
||||
self.volume_replicated,
|
||||
valid_target_device_id)
|
||||
|
||||
@mock.patch('hpe3parclient.version', "4.0.2")
|
||||
@mock.patch.object(volume_types, 'get_volume_type')
|
||||
def test_replication_failover_unmanaged(self, _mock_volume_types):
|
||||
# periodic vs. sync is not relevant when conducting a failover. We
|
||||
# will just use periodic.
|
||||
provider_location = self.CLIENT_ID + ":" + self.REPLICATION_CLIENT_ID
|
||||
conf = self.setup_configuration()
|
||||
self.replication_devs_unmgd[0]['replication_mode'] = 'periodic'
|
||||
conf.replication_device = self.replication_devs_unmgd
|
||||
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': 'periodic',
|
||||
'replication:sync_period': '900',
|
||||
'volume_type': self.volume_type_replicated}}
|
||||
|
||||
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
|
||||
valid_target_device_id = (
|
||||
self.replication_targets[0]['target_device_id'])
|
||||
invalid_target_device_id = 'INVALID'
|
||||
|
||||
# test invalid secondary target
|
||||
self.assertRaises(
|
||||
exception.VolumeBackendAPIException,
|
||||
self.driver.replication_failover,
|
||||
context.get_admin_context(),
|
||||
self.volume_replicated,
|
||||
invalid_target_device_id)
|
||||
|
||||
# test no secondary target
|
||||
self.assertRaises(
|
||||
exception.VolumeBackendAPIException,
|
||||
self.driver.replication_failover,
|
||||
context.get_admin_context(),
|
||||
self.volume_replicated,
|
||||
None)
|
||||
|
||||
# test a successful failover
|
||||
volume = self.volume_replicated
|
||||
volume['provider_location'] = self.CLIENT_ID
|
||||
return_model = self.driver.replication_failover(
|
||||
context.get_admin_context(),
|
||||
volume,
|
||||
valid_target_device_id)
|
||||
expected = [
|
||||
mock.call.stopRemoteCopy(self.RCG_3PAR_NAME)]
|
||||
mock_client.assert_has_calls(
|
||||
self.get_id_login +
|
||||
self.standard_logout +
|
||||
self.standard_login +
|
||||
expected +
|
||||
self.standard_logout)
|
||||
self.assertEqual({'replication_status': 'inactive',
|
||||
'provider_location': provider_location},
|
||||
return_model)
|
||||
|
||||
# test a unsuccessful failover
|
||||
mock_replicated_client.recoverRemoteCopyGroupFromDisaster.\
|
||||
side_effect = (
|
||||
exception.VolumeBackendAPIException(
|
||||
"Error: Failover was unsuccessful."))
|
||||
self.assertRaises(
|
||||
exception.VolumeBackendAPIException,
|
||||
self.driver.replication_failover,
|
||||
context.get_admin_context(),
|
||||
self.volume_replicated,
|
||||
valid_target_device_id)
|
||||
|
||||
|
||||
class TestHPE3PARFCDriver(HPE3PARBaseDriver, test.TestCase):
|
||||
|
||||
@@ -6035,7 +6305,11 @@ class TestHPE3PARISCSIDriver(HPE3PARBaseDriver, test.TestCase):
|
||||
def test_get_least_used_nsp_for_host_single(self):
|
||||
# setup_mock_client drive with default configuration
|
||||
# and return the mock HTTP 3PAR client
|
||||
mock_client = self.setup_driver()
|
||||
|
||||
# Setup two ISCSI IPs
|
||||
conf = self.setup_configuration()
|
||||
conf.hpe3par_iscsi_ips = ["10.10.220.253"]
|
||||
mock_client = self.setup_driver(config=conf)
|
||||
|
||||
mock_client.getPorts.return_value = PORTS_RET
|
||||
mock_client.getVLUNs.return_value = VLUNS1_RET
|
||||
@@ -6045,10 +6319,6 @@ class TestHPE3PARISCSIDriver(HPE3PARBaseDriver, test.TestCase):
|
||||
mock_create_client.return_value = mock_client
|
||||
common = self.driver._login()
|
||||
|
||||
# Setup a single ISCSI IP
|
||||
iscsi_ips = ["10.10.220.253"]
|
||||
self.driver.configuration.hpe3par_iscsi_ips = iscsi_ips
|
||||
|
||||
self.driver.initialize_iscsi_ports(common)
|
||||
|
||||
nsp = self.driver._get_least_used_nsp_for_host(common, 'newhost')
|
||||
@@ -6057,7 +6327,11 @@ class TestHPE3PARISCSIDriver(HPE3PARBaseDriver, test.TestCase):
|
||||
def test_get_least_used_nsp_for_host_new(self):
|
||||
# setup_mock_client drive with default configuration
|
||||
# and return the mock HTTP 3PAR client
|
||||
mock_client = self.setup_driver()
|
||||
|
||||
# Setup two ISCSI IPs
|
||||
conf = self.setup_configuration()
|
||||
conf.hpe3par_iscsi_ips = ["10.10.220.252", "10.10.220.253"]
|
||||
mock_client = self.setup_driver(config=conf)
|
||||
|
||||
mock_client.getPorts.return_value = PORTS_RET
|
||||
mock_client.getVLUNs.return_value = VLUNS1_RET
|
||||
@@ -6067,10 +6341,6 @@ class TestHPE3PARISCSIDriver(HPE3PARBaseDriver, test.TestCase):
|
||||
mock_create_client.return_value = mock_client
|
||||
common = self.driver._login()
|
||||
|
||||
# Setup two ISCSI IPs
|
||||
iscsi_ips = ["10.10.220.252", "10.10.220.253"]
|
||||
self.driver.configuration.hpe3par_iscsi_ips = iscsi_ips
|
||||
|
||||
self.driver.initialize_iscsi_ports(common)
|
||||
|
||||
# Host 'newhost' does not yet have any iscsi paths,
|
||||
@@ -6081,7 +6351,11 @@ class TestHPE3PARISCSIDriver(HPE3PARBaseDriver, test.TestCase):
|
||||
def test_get_least_used_nsp_for_host_reuse(self):
|
||||
# setup_mock_client drive with default configuration
|
||||
# and return the mock HTTP 3PAR client
|
||||
mock_client = self.setup_driver()
|
||||
|
||||
# Setup two ISCSI IPs
|
||||
conf = self.setup_configuration()
|
||||
conf.hpe3par_iscsi_ips = ["10.10.220.252", "10.10.220.253"]
|
||||
mock_client = self.setup_driver(config=conf)
|
||||
|
||||
mock_client.getPorts.return_value = PORTS_RET
|
||||
mock_client.getVLUNs.return_value = VLUNS1_RET
|
||||
@@ -6091,10 +6365,6 @@ class TestHPE3PARISCSIDriver(HPE3PARBaseDriver, test.TestCase):
|
||||
mock_create_client.return_value = mock_client
|
||||
common = self.driver._login()
|
||||
|
||||
# Setup two ISCSI IPs
|
||||
iscsi_ips = ["10.10.220.252", "10.10.220.253"]
|
||||
self.driver.configuration.hpe3par_iscsi_ips = iscsi_ips
|
||||
|
||||
self.driver.initialize_iscsi_ports(common)
|
||||
|
||||
# hosts 'foo' and 'bar' already have active iscsi paths
|
||||
|
||||
@@ -216,10 +216,11 @@ class HPE3PARCommon(object):
|
||||
3.0.2 - Python 3 support
|
||||
3.0.3 - Remove db access for consistency groups
|
||||
3.0.4 - Adds v2 managed replication support
|
||||
3.0.5 - Adds v2 unmanaged replication support
|
||||
|
||||
"""
|
||||
|
||||
VERSION = "3.0.4"
|
||||
VERSION = "3.0.5"
|
||||
|
||||
stats = {}
|
||||
|
||||
@@ -241,6 +242,7 @@ class HPE3PARCommon(object):
|
||||
PERIODIC = 2
|
||||
EXTRA_SPEC_REP_MODE = "replication:mode"
|
||||
EXTRA_SPEC_REP_SYNC_PERIOD = "replication:sync_period"
|
||||
RC_ACTION_CHANGE_TO_PRIMARY = 7
|
||||
|
||||
# Valid values for volume type extra specs
|
||||
# The first value in the list is the default value
|
||||
@@ -266,6 +268,7 @@ class HPE3PARCommon(object):
|
||||
self.config = config
|
||||
self.client = None
|
||||
self.uuid = uuid.uuid4()
|
||||
self._client_conf = {}
|
||||
self._replication_targets = []
|
||||
self._replication_enabled = False
|
||||
|
||||
@@ -279,14 +282,22 @@ class HPE3PARCommon(object):
|
||||
LOG.error(msg)
|
||||
raise exception.InvalidInput(reason=msg)
|
||||
|
||||
def check_replication_flags(self, options, required_flags):
|
||||
for flag in required_flags:
|
||||
if not options.get(flag, None):
|
||||
msg = (_('%s is not set and is required for the replicaiton '
|
||||
'device to be valid.') % flag)
|
||||
LOG.error(msg)
|
||||
raise exception.InvalidInput(reason=msg)
|
||||
|
||||
def _create_client(self, timeout=None):
|
||||
hpe3par_api_url = self._client_conf['hpe3par_api_url']
|
||||
# Timeout is only supported in version 4.0.2 and greater of the
|
||||
# python-3parclient.
|
||||
if hpe3parclient.version >= MIN_REP_CLIENT_VERSION:
|
||||
cl = client.HPE3ParClient(self.config.hpe3par_api_url,
|
||||
timeout=timeout)
|
||||
cl = client.HPE3ParClient(hpe3par_api_url, timeout=timeout)
|
||||
else:
|
||||
cl = client.HPE3ParClient(self.config.hpe3par_api_url)
|
||||
cl = client.HPE3ParClient(hpe3par_api_url)
|
||||
client_version = hpe3parclient.version
|
||||
|
||||
if client_version < MIN_CLIENT_VERSION:
|
||||
@@ -304,11 +315,11 @@ class HPE3PARCommon(object):
|
||||
def client_login(self):
|
||||
try:
|
||||
LOG.debug("Connecting to 3PAR")
|
||||
self.client.login(self.config.hpe3par_username,
|
||||
self.config.hpe3par_password)
|
||||
self.client.login(self._client_conf['hpe3par_username'],
|
||||
self._client_conf['hpe3par_password'])
|
||||
except hpeexceptions.HTTPUnauthorized as ex:
|
||||
msg = (_("Failed to Login to 3PAR (%(url)s) because %(err)s") %
|
||||
{'url': self.config.hpe3par_api_url, 'err': ex})
|
||||
{'url': self._client_conf['hpe3par_api_url'], 'err': ex})
|
||||
LOG.error(msg)
|
||||
raise exception.InvalidInput(reason=msg)
|
||||
|
||||
@@ -317,12 +328,12 @@ class HPE3PARCommon(object):
|
||||
if CONF.strict_ssh_host_key_policy:
|
||||
policy = "RejectPolicy"
|
||||
self.client.setSSHOptions(
|
||||
self.config.san_ip,
|
||||
self.config.san_login,
|
||||
self.config.san_password,
|
||||
port=self.config.san_ssh_port,
|
||||
conn_timeout=self.config.ssh_conn_timeout,
|
||||
privatekey=self.config.san_private_key,
|
||||
self._client_conf['san_ip'],
|
||||
self._client_conf['san_login'],
|
||||
self._client_conf['san_password'],
|
||||
port=self._client_conf['san_ssh_port'],
|
||||
conn_timeout=self._client_conf['ssh_conn_timeout'],
|
||||
privatekey=self._client_conf['san_private_key'],
|
||||
missing_key_policy=policy,
|
||||
known_hosts_file=known_hosts_file)
|
||||
|
||||
@@ -357,9 +368,10 @@ class HPE3PARCommon(object):
|
||||
return cl
|
||||
|
||||
def _destroy_replication_client(self, client):
|
||||
client.logout()
|
||||
if client is not None:
|
||||
client.logout()
|
||||
|
||||
def do_setup(self, context, timeout=None):
|
||||
def do_setup(self, context, volume=None, timeout=None):
|
||||
if hpe3parclient is None:
|
||||
msg = _('You must install hpe3parclient before using 3PAR'
|
||||
' drivers. Run "pip install python-3parclient" to'
|
||||
@@ -367,6 +379,11 @@ class HPE3PARCommon(object):
|
||||
raise exception.VolumeBackendAPIException(data=msg)
|
||||
|
||||
try:
|
||||
# This will set self._client_conf with the proper credentials
|
||||
# to communicate with the 3PAR array. It will contain either
|
||||
# the values for the primary array or secondary array in the
|
||||
# case of a fail-over.
|
||||
self._get_3par_config(volume)
|
||||
self.client = self._create_client(timeout=timeout)
|
||||
wsapi_version = self.client.getWsApiVersion()
|
||||
self.API_VERSION = wsapi_version['build']
|
||||
@@ -416,11 +433,6 @@ class HPE3PARCommon(object):
|
||||
finally:
|
||||
self.client_logout()
|
||||
|
||||
# v2 replication setup
|
||||
if not self._replication_enabled and (
|
||||
hpe3parclient.version >= MIN_REP_CLIENT_VERSION):
|
||||
self._do_replication_setup()
|
||||
|
||||
def check_for_setup_error(self):
|
||||
if self.client:
|
||||
self.client_login()
|
||||
@@ -943,6 +955,9 @@ class HPE3PARCommon(object):
|
||||
if 'must be in the same domain' in e.get_description():
|
||||
LOG.error(e.get_description())
|
||||
raise exception.Invalid3PARDomain(err=e.get_description())
|
||||
else:
|
||||
raise exception.VolumeBackendAPIException(
|
||||
data=e.get_description())
|
||||
|
||||
def _safe_hostname(self, hostname):
|
||||
"""We have to use a safe hostname length for 3PAR host names."""
|
||||
@@ -2693,7 +2708,7 @@ class HPE3PARCommon(object):
|
||||
volume['id'], volume['provider_location'])
|
||||
cl = self._create_replication_client(failover_target)
|
||||
cl.recoverRemoteCopyGroupFromDisaster(
|
||||
remote_rcg_name, self.client.RC_ACTION_CHANGE_TO_PRIMARY)
|
||||
remote_rcg_name, self.RC_ACTION_CHANGE_TO_PRIMARY)
|
||||
new_location = volume['provider_location'] + ":" + (
|
||||
failover_target['id'])
|
||||
|
||||
@@ -2740,9 +2755,16 @@ class HPE3PARCommon(object):
|
||||
"not be verified with the primary array."))
|
||||
|
||||
replication_targets = []
|
||||
volume_type = self._get_volume_type(volume["volume_type_id"])
|
||||
extra_specs = volume_type.get("extra_specs")
|
||||
replication_mode = extra_specs.get(self.EXTRA_SPEC_REP_MODE)
|
||||
replication_mode_num = self._get_remote_copy_mode_num(
|
||||
replication_mode)
|
||||
|
||||
for target in self._replication_targets:
|
||||
if not allowed_names or (
|
||||
target['target_device_id'] in allowed_names):
|
||||
if not allowed_names and replication_mode_num == (
|
||||
target['replication_mode']) or (
|
||||
target['target_device_id'] in allowed_names):
|
||||
list_vals = {'target_device_id': target['target_device_id']}
|
||||
replication_targets.append(list_vals)
|
||||
|
||||
@@ -2750,49 +2772,36 @@ class HPE3PARCommon(object):
|
||||
'targets': replication_targets}
|
||||
|
||||
def _do_replication_setup(self):
|
||||
replication_targets = []
|
||||
replication_devices = self.config.replication_device
|
||||
if replication_devices:
|
||||
for dev in replication_devices:
|
||||
remote_array = {}
|
||||
is_managed = dev.get('managed_backend_name')
|
||||
if not is_managed:
|
||||
msg = _("Unmanaged replication is not supported at this "
|
||||
"time. Please configure cinder.conf for managed "
|
||||
"replication.")
|
||||
LOG.error(msg)
|
||||
raise exception.VolumeBackendAPIException(data=msg)
|
||||
|
||||
remote_array['managed_backend_name'] = is_managed
|
||||
remote_array = dict(dev.items())
|
||||
# Override and set defaults for certain entries
|
||||
remote_array['managed_backend_name'] = (
|
||||
dev.get('managed_backend_name'))
|
||||
remote_array['replication_mode'] = (
|
||||
self._get_remote_copy_mode_num(
|
||||
dev.get('replication_mode')))
|
||||
remote_array['target_device_id'] = (
|
||||
dev.get('target_device_id'))
|
||||
remote_array['cpg_map'] = (
|
||||
dev.get('cpg_map'))
|
||||
remote_array['hpe3par_api_url'] = (
|
||||
dev.get('hpe3par_api_url'))
|
||||
remote_array['hpe3par_username'] = (
|
||||
dev.get('hpe3par_username'))
|
||||
remote_array['hpe3par_password'] = (
|
||||
dev.get('hpe3par_password'))
|
||||
remote_array['san_ip'] = (
|
||||
dev.get('san_ip'))
|
||||
remote_array['san_login'] = (
|
||||
dev.get('san_login'))
|
||||
remote_array['san_password'] = (
|
||||
dev.get('san_password'))
|
||||
remote_array['san_ssh_port'] = (
|
||||
dev.get('san_ssh_port', self.config.san_ssh_port))
|
||||
remote_array['ssh_conn_timeout'] = (
|
||||
dev.get('ssh_conn_timeout', self.config.ssh_conn_timeout))
|
||||
remote_array['san_private_key'] = (
|
||||
dev.get('san_private_key', self.config.san_private_key))
|
||||
# Format iscsi IPs correctly
|
||||
iscsi_ips = dev.get('hpe3par_iscsi_ips')
|
||||
if iscsi_ips:
|
||||
remote_array['hpe3par_iscsi_ips'] = iscsi_ips.split(' ')
|
||||
# Format hpe3par_iscsi_chap_enabled as a bool
|
||||
remote_array['hpe3par_iscsi_chap_enabled'] = (
|
||||
dev.get('hpe3par_iscsi_chap_enabled') == 'True')
|
||||
array_name = remote_array['target_device_id']
|
||||
|
||||
# Make sure we can log into the client, that it has been
|
||||
# correctly configured, and it its version matches the
|
||||
# primary arrarys version.
|
||||
cl = None
|
||||
try:
|
||||
cl = self._create_replication_client(remote_array)
|
||||
array_id = six.text_type(cl.getStorageSystemInfo()['id'])
|
||||
@@ -2816,13 +2825,14 @@ class HPE3PARCommon(object):
|
||||
"In order to be valid, target_device_id, "
|
||||
"replication_mode, "
|
||||
"hpe3par_api_url, hpe3par_username, "
|
||||
"hpe3par_password, cpg_map, and "
|
||||
"hpe3par_password, cpg_map, san_ip, "
|
||||
"san_login, and san_password "
|
||||
"must be specified. If the target is "
|
||||
"managed, managed_backend_name must be set "
|
||||
"as well.") % array_name)
|
||||
LOG.warning(msg)
|
||||
else:
|
||||
self._replication_targets.append(remote_array)
|
||||
replication_targets.append(remote_array)
|
||||
except Exception:
|
||||
msg = (_LE("Could not log in to 3PAR array (%s) with the "
|
||||
"provided credentials.") % array_name)
|
||||
@@ -2830,14 +2840,20 @@ class HPE3PARCommon(object):
|
||||
finally:
|
||||
self._destroy_replication_client(cl)
|
||||
|
||||
self._replication_targets = replication_targets
|
||||
if self._is_replication_configured_correct():
|
||||
self._replication_enabled = True
|
||||
|
||||
def _is_valid_replication_array(self, target):
|
||||
for k, v in target.items():
|
||||
if v is None:
|
||||
return False
|
||||
return True
|
||||
required_flags = ['hpe3par_api_url', 'hpe3par_username',
|
||||
'hpe3par_password', 'san_ip', 'san_login',
|
||||
'san_password', 'target_device_id',
|
||||
'replication_mode', 'cpg_map']
|
||||
try:
|
||||
self.check_replication_flags(target, required_flags)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _is_replication_configured_correct(self):
|
||||
rep_flag = True
|
||||
@@ -2896,6 +2912,73 @@ class HPE3PARCommon(object):
|
||||
ret_mode = self.PERIODIC
|
||||
return ret_mode
|
||||
|
||||
def _get_3par_config(self, volume):
|
||||
if hpe3parclient.version >= MIN_REP_CLIENT_VERSION:
|
||||
self._do_replication_setup()
|
||||
conf = None
|
||||
if self._replication_enabled and volume:
|
||||
provider_location = volume.get('provider_location')
|
||||
if provider_location:
|
||||
if volume.get('replication_status') == 'failed-over':
|
||||
_, provider_location = provider_location.split(':')
|
||||
|
||||
for target in self._replication_targets:
|
||||
if target['id'] == provider_location:
|
||||
conf = target
|
||||
break
|
||||
self._build_3par_config(conf)
|
||||
|
||||
def _build_3par_config(self, conf=None):
|
||||
"""Build 3PAR client config dictionary.
|
||||
|
||||
self._client_conf will contain values from self.config if the volume
|
||||
is located on the primary array in order to properly contact it. If
|
||||
the volume has been failed over and therefore on a secondary array,
|
||||
self._client_conf will contain values on how to contact that array.
|
||||
The only time we will return with entries from a secondary array is
|
||||
with unmanaged replication.
|
||||
"""
|
||||
if conf:
|
||||
self._client_conf['hpe3par_username'] = (
|
||||
conf.get('hpe3par_username'))
|
||||
self._client_conf['hpe3par_password'] = (
|
||||
conf.get('hpe3par_password'))
|
||||
self._client_conf['san_ip'] = conf.get('san_ip')
|
||||
self._client_conf['san_login'] = conf.get('san_login')
|
||||
self._client_conf['san_password'] = conf.get('san_password')
|
||||
self._client_conf['san_ssh_port'] = conf.get('san_ssh_port')
|
||||
self._client_conf['ssh_conn_timeout'] = (
|
||||
conf.get('ssh_conn_timeout'))
|
||||
self._client_conf['san_private_key'] = conf.get('san_private_key')
|
||||
self._client_conf['hpe3par_api_url'] = conf.get('hpe3par_api_url')
|
||||
self._client_conf['hpe3par_iscsi_ips'] = (
|
||||
conf.get('hpe3par_iscsi_ips'))
|
||||
self._client_conf['hpe3par_iscsi_chap_enabled'] = (
|
||||
conf.get('hpe3par_iscsi_chap_enabled'))
|
||||
self._client_conf['iscsi_ip_address'] = (
|
||||
conf.get('iscsi_ip_address'))
|
||||
self._client_conf['iscsi_port'] = conf.get('iscsi_port')
|
||||
else:
|
||||
self._client_conf['hpe3par_username'] = (
|
||||
self.config.hpe3par_username)
|
||||
self._client_conf['hpe3par_password'] = (
|
||||
self.config.hpe3par_password)
|
||||
self._client_conf['san_ip'] = self.config.san_ip
|
||||
self._client_conf['san_login'] = self.config.san_login
|
||||
self._client_conf['san_password'] = self.config.san_password
|
||||
self._client_conf['san_ssh_port'] = self.config.san_ssh_port
|
||||
self._client_conf['ssh_conn_timeout'] = (
|
||||
self.config.ssh_conn_timeout)
|
||||
self._client_conf['san_private_key'] = self.config.san_private_key
|
||||
self._client_conf['hpe3par_api_url'] = self.config.hpe3par_api_url
|
||||
self._client_conf['hpe3par_iscsi_ips'] = (
|
||||
self.config.hpe3par_iscsi_ips)
|
||||
self._client_conf['hpe3par_iscsi_chap_enabled'] = (
|
||||
self.config.hpe3par_iscsi_chap_enabled)
|
||||
self._client_conf['iscsi_ip_address'] = (
|
||||
self.config.iscsi_ip_address)
|
||||
self._client_conf['iscsi_port'] = self.config.iscsi_port
|
||||
|
||||
def _get_cpg_from_cpg_map(self, cpg_map, target_cpg):
|
||||
ret_target_cpg = None
|
||||
cpg_pairs = cpg_map.split(' ')
|
||||
@@ -3096,8 +3179,9 @@ class HPE3PARCommon(object):
|
||||
# Do regular volume replication destroy now config mirroring is off
|
||||
try:
|
||||
self._do_volume_replication_destroy(volume, rcg_name)
|
||||
except Exception:
|
||||
msg = (_("The failed-over volume could not be deleted."))
|
||||
except Exception as ex:
|
||||
msg = (_("The failed-over volume could not be deleted: %s") %
|
||||
six.text_type(ex))
|
||||
LOG.error(msg)
|
||||
raise exception.VolumeIsBusy(message=msg)
|
||||
finally:
|
||||
|
||||
@@ -92,10 +92,11 @@ class HPE3PARFCDriver(driver.TransferVD,
|
||||
3.0.0 - Rebranded HP to HPE.
|
||||
3.0.1 - Remove db access for consistency groups
|
||||
3.0.2 - Adds v2 managed replication support
|
||||
3.0.3 - Adds v2 unmanaged replication support
|
||||
|
||||
"""
|
||||
|
||||
VERSION = "3.0.2"
|
||||
VERSION = "3.0.3"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(HPE3PARFCDriver, self).__init__(*args, **kwargs)
|
||||
@@ -106,12 +107,12 @@ class HPE3PARFCDriver(driver.TransferVD,
|
||||
def _init_common(self):
|
||||
return hpecommon.HPE3PARCommon(self.configuration)
|
||||
|
||||
def _login(self, timeout=None):
|
||||
def _login(self, volume=None, timeout=None):
|
||||
common = self._init_common()
|
||||
# If replication is enabled and we cannot login, we do not want to
|
||||
# raise an exception so a failover can still be executed.
|
||||
try:
|
||||
common.do_setup(None, timeout=timeout)
|
||||
common.do_setup(None, volume, timeout=timeout)
|
||||
common.client_login()
|
||||
except Exception:
|
||||
if common._replication_enabled:
|
||||
@@ -165,21 +166,21 @@ class HPE3PARFCDriver(driver.TransferVD,
|
||||
pass
|
||||
|
||||
def create_volume(self, volume):
|
||||
common = self._login()
|
||||
common = self._login(volume)
|
||||
try:
|
||||
return common.create_volume(volume)
|
||||
finally:
|
||||
self._logout(common)
|
||||
|
||||
def create_cloned_volume(self, volume, src_vref):
|
||||
common = self._login()
|
||||
common = self._login(volume)
|
||||
try:
|
||||
return common.create_cloned_volume(volume, src_vref)
|
||||
finally:
|
||||
self._logout(common)
|
||||
|
||||
def delete_volume(self, volume):
|
||||
common = self._login()
|
||||
common = self._login(volume)
|
||||
try:
|
||||
common.delete_volume(volume)
|
||||
finally:
|
||||
@@ -190,21 +191,21 @@ class HPE3PARFCDriver(driver.TransferVD,
|
||||
|
||||
TODO: support using the size from the user.
|
||||
"""
|
||||
common = self._login()
|
||||
common = self._login(volume)
|
||||
try:
|
||||
return common.create_volume_from_snapshot(volume, snapshot)
|
||||
finally:
|
||||
self._logout(common)
|
||||
|
||||
def create_snapshot(self, snapshot):
|
||||
common = self._login()
|
||||
common = self._login(snapshot['volume'])
|
||||
try:
|
||||
common.create_snapshot(snapshot)
|
||||
finally:
|
||||
self._logout(common)
|
||||
|
||||
def delete_snapshot(self, snapshot):
|
||||
common = self._login()
|
||||
common = self._login(snapshot['volume'])
|
||||
try:
|
||||
common.delete_snapshot(snapshot)
|
||||
finally:
|
||||
@@ -250,7 +251,7 @@ class HPE3PARFCDriver(driver.TransferVD,
|
||||
* Create a VLUN for that HOST with the volume we want to export.
|
||||
|
||||
"""
|
||||
common = self._login()
|
||||
common = self._login(volume)
|
||||
try:
|
||||
# we have to make sure we have a host
|
||||
host = self._create_host(common, volume, connector)
|
||||
@@ -293,7 +294,7 @@ class HPE3PARFCDriver(driver.TransferVD,
|
||||
@fczm_utils.RemoveFCZone
|
||||
def terminate_connection(self, volume, connector, **kwargs):
|
||||
"""Driver entry point to unattach a volume from an instance."""
|
||||
common = self._login()
|
||||
common = self._login(volume)
|
||||
try:
|
||||
hostname = common._safe_hostname(connector['host'])
|
||||
common.terminate_connection(volume, hostname,
|
||||
@@ -449,7 +450,7 @@ class HPE3PARFCDriver(driver.TransferVD,
|
||||
pass
|
||||
|
||||
def extend_volume(self, volume, new_size):
|
||||
common = self._login()
|
||||
common = self._login(volume)
|
||||
try:
|
||||
common.extend_volume(volume, new_size)
|
||||
finally:
|
||||
@@ -504,21 +505,21 @@ class HPE3PARFCDriver(driver.TransferVD,
|
||||
self._logout(common)
|
||||
|
||||
def manage_existing(self, volume, existing_ref):
|
||||
common = self._login()
|
||||
common = self._login(volume)
|
||||
try:
|
||||
return common.manage_existing(volume, existing_ref)
|
||||
finally:
|
||||
self._logout(common)
|
||||
|
||||
def manage_existing_get_size(self, volume, existing_ref):
|
||||
common = self._login()
|
||||
common = self._login(volume)
|
||||
try:
|
||||
return common.manage_existing_get_size(volume, existing_ref)
|
||||
finally:
|
||||
self._logout(common)
|
||||
|
||||
def unmanage(self, volume):
|
||||
common = self._login()
|
||||
common = self._login(volume)
|
||||
try:
|
||||
common.unmanage(volume)
|
||||
finally:
|
||||
@@ -526,14 +527,14 @@ class HPE3PARFCDriver(driver.TransferVD,
|
||||
|
||||
def attach_volume(self, context, volume, instance_uuid, host_name,
|
||||
mountpoint):
|
||||
common = self._login()
|
||||
common = self._login(volume)
|
||||
try:
|
||||
common.attach_volume(volume, instance_uuid)
|
||||
finally:
|
||||
self._logout(common)
|
||||
|
||||
def detach_volume(self, context, volume, attachment=None):
|
||||
common = self._login()
|
||||
common = self._login(volume)
|
||||
try:
|
||||
common.detach_volume(volume, attachment)
|
||||
finally:
|
||||
@@ -541,7 +542,7 @@ class HPE3PARFCDriver(driver.TransferVD,
|
||||
|
||||
def retype(self, context, volume, new_type, diff, host):
|
||||
"""Convert the volume to be of the new type."""
|
||||
common = self._login()
|
||||
common = self._login(volume)
|
||||
try:
|
||||
return common.retype(volume, new_type, diff, host)
|
||||
finally:
|
||||
@@ -555,7 +556,7 @@ class HPE3PARFCDriver(driver.TransferVD,
|
||||
"to a host with storage_protocol=%s.", protocol)
|
||||
return False, None
|
||||
|
||||
common = self._login()
|
||||
common = self._login(volume)
|
||||
try:
|
||||
return common.migrate_volume(volume, host)
|
||||
finally:
|
||||
@@ -564,7 +565,7 @@ class HPE3PARFCDriver(driver.TransferVD,
|
||||
def update_migrated_volume(self, context, volume, new_volume,
|
||||
original_volume_status):
|
||||
"""Update the name of the migrated volume to it's new ID."""
|
||||
common = self._login()
|
||||
common = self._login(volume)
|
||||
try:
|
||||
return common.update_migrated_volume(context, volume, new_volume,
|
||||
original_volume_status)
|
||||
@@ -572,7 +573,7 @@ class HPE3PARFCDriver(driver.TransferVD,
|
||||
self._logout(common)
|
||||
|
||||
def get_pool(self, volume):
|
||||
common = self._login()
|
||||
common = self._login(volume)
|
||||
try:
|
||||
return common.get_cpg(volume)
|
||||
except hpeexceptions.HTTPNotFound:
|
||||
@@ -591,7 +592,7 @@ class HPE3PARFCDriver(driver.TransferVD,
|
||||
|
||||
def replication_enable(self, context, volume):
|
||||
"""Enable replication on a replication capable volume."""
|
||||
common = self._login()
|
||||
common = self._login(volume)
|
||||
try:
|
||||
return common.replication_enable(context, volume)
|
||||
finally:
|
||||
@@ -599,7 +600,7 @@ class HPE3PARFCDriver(driver.TransferVD,
|
||||
|
||||
def replication_disable(self, context, volume):
|
||||
"""Disable replication on the specified volume."""
|
||||
common = self._login()
|
||||
common = self._login(volume)
|
||||
try:
|
||||
return common.replication_disable(context, volume)
|
||||
finally:
|
||||
@@ -607,7 +608,7 @@ class HPE3PARFCDriver(driver.TransferVD,
|
||||
|
||||
def replication_failover(self, context, volume, secondary):
|
||||
"""Force failover to a secondary replication target."""
|
||||
common = self._login(timeout=30)
|
||||
common = self._login(volume, timeout=30)
|
||||
try:
|
||||
return common.replication_failover(context, volume, secondary)
|
||||
finally:
|
||||
@@ -615,7 +616,7 @@ class HPE3PARFCDriver(driver.TransferVD,
|
||||
|
||||
def list_replication_targets(self, context, volume):
|
||||
"""Provides a means to obtain replication targets for a volume."""
|
||||
common = self._login(timeout=30)
|
||||
common = self._login(volume, timeout=30)
|
||||
try:
|
||||
return common.list_replication_targets(context, volume)
|
||||
finally:
|
||||
|
||||
@@ -104,10 +104,11 @@ class HPE3PARISCSIDriver(driver.TransferVD,
|
||||
3.0.2 - Remove db access for consistency groups
|
||||
3.0.3 - Fix multipath dictionary key error. bug #1522062
|
||||
3.0.4 - Adds v2 managed replication support
|
||||
3.0.5 - Adds v2 unmanaged replication support
|
||||
|
||||
"""
|
||||
|
||||
VERSION = "3.0.4"
|
||||
VERSION = "3.0.5"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(HPE3PARISCSIDriver, self).__init__(*args, **kwargs)
|
||||
@@ -117,12 +118,12 @@ class HPE3PARISCSIDriver(driver.TransferVD,
|
||||
def _init_common(self):
|
||||
return hpecommon.HPE3PARCommon(self.configuration)
|
||||
|
||||
def _login(self, timeout=None):
|
||||
def _login(self, volume=None, timeout=None):
|
||||
common = self._init_common()
|
||||
common.do_setup(None, timeout=timeout)
|
||||
# If replication is enabled and we cannot login, we do not want to
|
||||
# raise an exception so a failover can still be executed.
|
||||
try:
|
||||
common.do_setup(None, volume, timeout=timeout)
|
||||
common.client_login()
|
||||
except Exception:
|
||||
if common._replication_enabled:
|
||||
@@ -171,6 +172,7 @@ class HPE3PARISCSIDriver(driver.TransferVD,
|
||||
self._check_flags(common)
|
||||
common.check_for_setup_error()
|
||||
|
||||
self.iscsi_ips = {}
|
||||
common.client_login()
|
||||
try:
|
||||
self.initialize_iscsi_ports(common)
|
||||
@@ -181,13 +183,13 @@ class HPE3PARISCSIDriver(driver.TransferVD,
|
||||
# map iscsi_ip-> ip_port
|
||||
# -> iqn
|
||||
# -> nsp
|
||||
self.iscsi_ips = {}
|
||||
iscsi_ip_list = {}
|
||||
temp_iscsi_ip = {}
|
||||
|
||||
# use the 3PAR ip_addr list for iSCSI configuration
|
||||
if len(self.configuration.hpe3par_iscsi_ips) > 0:
|
||||
if len(common._client_conf['hpe3par_iscsi_ips']) > 0:
|
||||
# add port values to ip_addr, if necessary
|
||||
for ip_addr in self.configuration.hpe3par_iscsi_ips:
|
||||
for ip_addr in common._client_conf['hpe3par_iscsi_ips']:
|
||||
ip = ip_addr.split(':')
|
||||
if len(ip) == 1:
|
||||
temp_iscsi_ip[ip_addr] = {'ip_port': DEFAULT_ISCSI_PORT}
|
||||
@@ -199,9 +201,9 @@ class HPE3PARISCSIDriver(driver.TransferVD,
|
||||
# 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 (self.configuration.iscsi_ip_address not in temp_iscsi_ip):
|
||||
ip = self.configuration.iscsi_ip_address
|
||||
ip_port = self.configuration.iscsi_port
|
||||
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}
|
||||
|
||||
# get all the valid iSCSI ports from 3PAR
|
||||
@@ -213,17 +215,16 @@ class HPE3PARISCSIDriver(driver.TransferVD,
|
||||
ip = port['IPAddr']
|
||||
if ip in temp_iscsi_ip:
|
||||
ip_port = temp_iscsi_ip[ip]['ip_port']
|
||||
self.iscsi_ips[ip] = {'ip_port': ip_port,
|
||||
'nsp': port['nsp'],
|
||||
'iqn': port['iSCSIName']
|
||||
}
|
||||
iscsi_ip_list[ip] = {'ip_port': ip_port,
|
||||
'nsp': port['nsp'],
|
||||
'iqn': port['iSCSIName']}
|
||||
del temp_iscsi_ip[ip]
|
||||
|
||||
# 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 (self.configuration.iscsi_ip_address in temp_iscsi_ip):
|
||||
del temp_iscsi_ip[self.configuration.iscsi_ip_address]
|
||||
if common._client_conf['iscsi_ip_address'] in temp_iscsi_ip:
|
||||
del temp_iscsi_ip[common._client_conf['iscsi_ip_address']]
|
||||
|
||||
# lets see if there are invalid iSCSI IPs left in the temp dict
|
||||
if len(temp_iscsi_ip) > 0:
|
||||
@@ -232,17 +233,18 @@ class HPE3PARISCSIDriver(driver.TransferVD,
|
||||
"iscsi_ip_address '%s.'"),
|
||||
(", ".join(temp_iscsi_ip)))
|
||||
|
||||
if not len(self.iscsi_ips) > 0:
|
||||
if not len(iscsi_ip_list) > 0:
|
||||
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
|
||||
|
||||
def check_for_setup_error(self):
|
||||
"""Setup errors are already checked for in do_setup so return pass."""
|
||||
pass
|
||||
|
||||
def create_volume(self, volume):
|
||||
common = self._login()
|
||||
common = self._login(volume)
|
||||
try:
|
||||
return common.create_volume(volume)
|
||||
finally:
|
||||
@@ -250,14 +252,14 @@ class HPE3PARISCSIDriver(driver.TransferVD,
|
||||
|
||||
def create_cloned_volume(self, volume, src_vref):
|
||||
"""Clone an existing volume."""
|
||||
common = self._login()
|
||||
common = self._login(volume)
|
||||
try:
|
||||
return common.create_cloned_volume(volume, src_vref)
|
||||
finally:
|
||||
self._logout(common)
|
||||
|
||||
def delete_volume(self, volume):
|
||||
common = self._login()
|
||||
common = self._login(volume)
|
||||
try:
|
||||
common.delete_volume(volume)
|
||||
finally:
|
||||
@@ -268,21 +270,21 @@ class HPE3PARISCSIDriver(driver.TransferVD,
|
||||
|
||||
TODO: support using the size from the user.
|
||||
"""
|
||||
common = self._login()
|
||||
common = self._login(volume)
|
||||
try:
|
||||
return common.create_volume_from_snapshot(volume, snapshot)
|
||||
finally:
|
||||
self._logout(common)
|
||||
|
||||
def create_snapshot(self, snapshot):
|
||||
common = self._login()
|
||||
common = self._login(snapshot['volume'])
|
||||
try:
|
||||
common.create_snapshot(snapshot)
|
||||
finally:
|
||||
self._logout(common)
|
||||
|
||||
def delete_snapshot(self, snapshot):
|
||||
common = self._login()
|
||||
common = self._login(snapshot['volume'])
|
||||
try:
|
||||
common.delete_snapshot(snapshot)
|
||||
finally:
|
||||
@@ -314,8 +316,17 @@ class HPE3PARISCSIDriver(driver.TransferVD,
|
||||
* Create a host on the 3par
|
||||
* create vlun on the 3par
|
||||
"""
|
||||
common = self._login()
|
||||
common = self._login(volume)
|
||||
try:
|
||||
# If the volume has been failed over, we need to reinitialize
|
||||
# iSCSI ports so they represent the new array.
|
||||
if volume.get('replication_status') == 'failed-over' and (
|
||||
common._client_conf['hpe3par_api_url'] not in self.iscsi_ips):
|
||||
self.initialize_iscsi_ports(common)
|
||||
|
||||
# Grab the correct iSCSI ports
|
||||
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(
|
||||
common,
|
||||
@@ -331,7 +342,7 @@ class HPE3PARISCSIDriver(driver.TransferVD,
|
||||
target_luns = []
|
||||
|
||||
# Target portal ips are defined in cinder.conf.
|
||||
target_portal_ips = self.iscsi_ips.keys()
|
||||
target_portal_ips = iscsi_ips.keys()
|
||||
|
||||
# Collect all existing VLUNs for this volume/host combination.
|
||||
existing_vluns = common.find_existing_vluns(volume, host)
|
||||
@@ -347,15 +358,15 @@ class HPE3PARISCSIDriver(driver.TransferVD,
|
||||
# instead of creating a new VLUN.
|
||||
for v in existing_vluns:
|
||||
portPos = common.build_portPos(
|
||||
self.iscsi_ips[iscsi_ip]['nsp'])
|
||||
iscsi_ips[iscsi_ip]['nsp'])
|
||||
if v['portPos'] == portPos:
|
||||
vlun = v
|
||||
break
|
||||
else:
|
||||
vlun = common.create_vlun(
|
||||
volume, host, self.iscsi_ips[iscsi_ip]['nsp'])
|
||||
volume, host, iscsi_ips[iscsi_ip]['nsp'])
|
||||
iscsi_ip_port = "%s:%s" % (
|
||||
iscsi_ip, self.iscsi_ips[iscsi_ip]['ip_port'])
|
||||
iscsi_ip, iscsi_ips[iscsi_ip]['ip_port'])
|
||||
target_portals.append(iscsi_ip_port)
|
||||
target_iqns.append(port['iSCSIName'])
|
||||
target_luns.append(vlun['lun'])
|
||||
@@ -400,12 +411,12 @@ class HPE3PARISCSIDriver(driver.TransferVD,
|
||||
if least_used_nsp is None:
|
||||
LOG.warning(_LW("Least busy iSCSI port not found, "
|
||||
"using first iSCSI port in list."))
|
||||
iscsi_ip = self.iscsi_ips.keys()[0]
|
||||
iscsi_ip = iscsi_ips.keys()[0]
|
||||
else:
|
||||
iscsi_ip = self._get_ip_using_nsp(least_used_nsp)
|
||||
iscsi_ip = self._get_ip_using_nsp(least_used_nsp, common)
|
||||
|
||||
iscsi_ip_port = self.iscsi_ips[iscsi_ip]['ip_port']
|
||||
iscsi_target_iqn = self.iscsi_ips[iscsi_ip]['iqn']
|
||||
iscsi_ip_port = iscsi_ips[iscsi_ip]['ip_port']
|
||||
iscsi_target_iqn = iscsi_ips[iscsi_ip]['iqn']
|
||||
info = {'driver_volume_type': 'iscsi',
|
||||
'data': {'target_portal': "%s:%s" %
|
||||
(iscsi_ip, iscsi_ip_port),
|
||||
@@ -415,7 +426,7 @@ class HPE3PARISCSIDriver(driver.TransferVD,
|
||||
}
|
||||
}
|
||||
|
||||
if self.configuration.hpe3par_iscsi_chap_enabled:
|
||||
if common._client_conf['hpe3par_iscsi_chap_enabled']:
|
||||
info['data']['auth_method'] = 'CHAP'
|
||||
info['data']['auth_username'] = username
|
||||
info['data']['auth_password'] = password
|
||||
@@ -429,7 +440,7 @@ class HPE3PARISCSIDriver(driver.TransferVD,
|
||||
|
||||
def terminate_connection(self, volume, connector, **kwargs):
|
||||
"""Driver entry point to unattach a volume from an instance."""
|
||||
common = self._login()
|
||||
common = self._login(volume)
|
||||
try:
|
||||
hostname = common._safe_hostname(connector['host'])
|
||||
common.terminate_connection(
|
||||
@@ -497,7 +508,7 @@ class HPE3PARISCSIDriver(driver.TransferVD,
|
||||
|
||||
def _set_3par_chaps(self, common, hostname, volume, username, password):
|
||||
"""Sets a 3PAR host's CHAP credentials."""
|
||||
if not self.configuration.hpe3par_iscsi_chap_enabled:
|
||||
if not common._client_conf['hpe3par_iscsi_chap_enabled']:
|
||||
return
|
||||
|
||||
mod_request = {'chapOperation': common.client.HOST_EDIT_ADD,
|
||||
@@ -517,7 +528,7 @@ class HPE3PARISCSIDriver(driver.TransferVD,
|
||||
domain = common.get_domain(cpg)
|
||||
|
||||
# Get the CHAP secret if CHAP is enabled
|
||||
if self.configuration.hpe3par_iscsi_chap_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']
|
||||
@@ -550,7 +561,7 @@ class HPE3PARISCSIDriver(driver.TransferVD,
|
||||
password)
|
||||
host = common._get_3par_host(hostname)
|
||||
elif (not host['initiatorChapEnabled'] and
|
||||
self.configuration.hpe3par_iscsi_chap_enabled):
|
||||
common._client_conf['hpe3par_iscsi_chap_enabled']):
|
||||
LOG.warning(_LW("Host exists without CHAP credentials set and "
|
||||
"has iSCSI attachments but CHAP is enabled. "
|
||||
"Updating host with new CHAP credentials."))
|
||||
@@ -567,7 +578,7 @@ class HPE3PARISCSIDriver(driver.TransferVD,
|
||||
"""Gets the associated account, generates CHAP info and updates."""
|
||||
model_update = {}
|
||||
|
||||
if not self.configuration.hpe3par_iscsi_chap_enabled:
|
||||
if not common._client_conf['hpe3par_iscsi_chap_enabled']:
|
||||
model_update['provider_auth'] = None
|
||||
return model_update
|
||||
|
||||
@@ -636,7 +647,7 @@ class HPE3PARISCSIDriver(driver.TransferVD,
|
||||
return model_update
|
||||
|
||||
def create_export(self, context, volume, connector):
|
||||
common = self._login()
|
||||
common = self._login(volume)
|
||||
try:
|
||||
return self._do_export(common, volume)
|
||||
finally:
|
||||
@@ -647,7 +658,7 @@ class HPE3PARISCSIDriver(driver.TransferVD,
|
||||
|
||||
Also retrieves CHAP credentials, if present on the volume
|
||||
"""
|
||||
common = self._login()
|
||||
common = self._login(volume)
|
||||
try:
|
||||
vol_name = common._get_3par_vol_name(volume['id'])
|
||||
common.client.getVolume(vol_name)
|
||||
@@ -687,7 +698,7 @@ class HPE3PARISCSIDriver(driver.TransferVD,
|
||||
* Return NSP with fewest active vluns
|
||||
"""
|
||||
|
||||
iscsi_nsps = self._get_iscsi_nsps()
|
||||
iscsi_nsps = self._get_iscsi_nsps(common)
|
||||
# If there's only one path, use it
|
||||
if len(iscsi_nsps) == 1:
|
||||
return iscsi_nsps[0]
|
||||
@@ -705,19 +716,21 @@ class HPE3PARISCSIDriver(driver.TransferVD,
|
||||
# Calculate the least used iscsi nsp
|
||||
least_used_nsp = self._get_least_used_nsp(common,
|
||||
vluns['members'],
|
||||
self._get_iscsi_nsps())
|
||||
self._get_iscsi_nsps(common))
|
||||
return least_used_nsp
|
||||
|
||||
def _get_iscsi_nsps(self):
|
||||
def _get_iscsi_nsps(self, common):
|
||||
"""Return the list of candidate nsps."""
|
||||
nsps = []
|
||||
for value in self.iscsi_ips.values():
|
||||
iscsi_ips = self.iscsi_ips[common._client_conf['hpe3par_api_url']]
|
||||
for value in iscsi_ips.values():
|
||||
nsps.append(value['nsp'])
|
||||
return nsps
|
||||
|
||||
def _get_ip_using_nsp(self, nsp):
|
||||
def _get_ip_using_nsp(self, nsp, common):
|
||||
"""Return IP associated with given nsp."""
|
||||
for (key, value) in self.iscsi_ips.items():
|
||||
iscsi_ips = self.iscsi_ips[common._client_conf['hpe3par_api_url']]
|
||||
for (key, value) in iscsi_ips.items():
|
||||
if value['nsp'] == nsp:
|
||||
return key
|
||||
|
||||
@@ -748,7 +761,7 @@ class HPE3PARISCSIDriver(driver.TransferVD,
|
||||
return current_least_used_nsp
|
||||
|
||||
def extend_volume(self, volume, new_size):
|
||||
common = self._login()
|
||||
common = self._login(volume)
|
||||
try:
|
||||
common.extend_volume(volume, new_size)
|
||||
finally:
|
||||
@@ -803,21 +816,21 @@ class HPE3PARISCSIDriver(driver.TransferVD,
|
||||
self._logout(common)
|
||||
|
||||
def manage_existing(self, volume, existing_ref):
|
||||
common = self._login()
|
||||
common = self._login(volume)
|
||||
try:
|
||||
return common.manage_existing(volume, existing_ref)
|
||||
finally:
|
||||
self._logout(common)
|
||||
|
||||
def manage_existing_get_size(self, volume, existing_ref):
|
||||
common = self._login()
|
||||
common = self._login(volume)
|
||||
try:
|
||||
return common.manage_existing_get_size(volume, existing_ref)
|
||||
finally:
|
||||
self._logout(common)
|
||||
|
||||
def unmanage(self, volume):
|
||||
common = self._login()
|
||||
common = self._login(volume)
|
||||
try:
|
||||
common.unmanage(volume)
|
||||
finally:
|
||||
@@ -825,14 +838,14 @@ class HPE3PARISCSIDriver(driver.TransferVD,
|
||||
|
||||
def attach_volume(self, context, volume, instance_uuid, host_name,
|
||||
mountpoint):
|
||||
common = self._login()
|
||||
common = self._login(volume)
|
||||
try:
|
||||
common.attach_volume(volume, instance_uuid)
|
||||
finally:
|
||||
self._logout(common)
|
||||
|
||||
def detach_volume(self, context, volume, attachment=None):
|
||||
common = self._login()
|
||||
common = self._login(volume)
|
||||
try:
|
||||
common.detach_volume(volume, attachment)
|
||||
finally:
|
||||
@@ -840,7 +853,7 @@ class HPE3PARISCSIDriver(driver.TransferVD,
|
||||
|
||||
def retype(self, context, volume, new_type, diff, host):
|
||||
"""Convert the volume to be of the new type."""
|
||||
common = self._login()
|
||||
common = self._login(volume)
|
||||
try:
|
||||
return common.retype(volume, new_type, diff, host)
|
||||
finally:
|
||||
@@ -854,7 +867,7 @@ class HPE3PARISCSIDriver(driver.TransferVD,
|
||||
"to a host with storage_protocol=%s.", protocol)
|
||||
return False, None
|
||||
|
||||
common = self._login()
|
||||
common = self._login(volume)
|
||||
try:
|
||||
return common.migrate_volume(volume, host)
|
||||
finally:
|
||||
@@ -863,7 +876,7 @@ class HPE3PARISCSIDriver(driver.TransferVD,
|
||||
def update_migrated_volume(self, context, volume, new_volume,
|
||||
original_volume_status):
|
||||
"""Update the name of the migrated volume to it's new ID."""
|
||||
common = self._login()
|
||||
common = self._login(volume)
|
||||
try:
|
||||
return common.update_migrated_volume(context, volume, new_volume,
|
||||
original_volume_status)
|
||||
@@ -871,7 +884,7 @@ class HPE3PARISCSIDriver(driver.TransferVD,
|
||||
self._logout(common)
|
||||
|
||||
def get_pool(self, volume):
|
||||
common = self._login()
|
||||
common = self._login(volume)
|
||||
try:
|
||||
return common.get_cpg(volume)
|
||||
except hpeexceptions.HTTPNotFound:
|
||||
@@ -890,7 +903,7 @@ class HPE3PARISCSIDriver(driver.TransferVD,
|
||||
|
||||
def replication_enable(self, context, volume):
|
||||
"""Enable replication on a replication capable volume."""
|
||||
common = self._login()
|
||||
common = self._login(volume)
|
||||
try:
|
||||
return common.replication_enable(context, volume)
|
||||
finally:
|
||||
@@ -898,7 +911,7 @@ class HPE3PARISCSIDriver(driver.TransferVD,
|
||||
|
||||
def replication_disable(self, context, volume):
|
||||
"""Disable replication on the specified volume."""
|
||||
common = self._login()
|
||||
common = self._login(volume)
|
||||
try:
|
||||
return common.replication_disable(context, volume)
|
||||
finally:
|
||||
@@ -906,7 +919,7 @@ class HPE3PARISCSIDriver(driver.TransferVD,
|
||||
|
||||
def replication_failover(self, context, volume, secondary):
|
||||
"""Force failover to a secondary replication target."""
|
||||
common = self._login(timeout=30)
|
||||
common = self._login(volume, timeout=30)
|
||||
try:
|
||||
return common.replication_failover(context, volume, secondary)
|
||||
finally:
|
||||
@@ -914,7 +927,7 @@ class HPE3PARISCSIDriver(driver.TransferVD,
|
||||
|
||||
def list_replication_targets(self, context, volume):
|
||||
"""Provides a means to obtain replication targets for a volume."""
|
||||
common = self._login(timeout=30)
|
||||
common = self._login(volume, timeout=30)
|
||||
try:
|
||||
return common.list_replication_targets(context, volume)
|
||||
finally:
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
---
|
||||
features:
|
||||
- Added unmanaged v2 replication support to the HPE 3PAR driver.
|
||||
Reference in New Issue
Block a user