LeftHand: Implement v2 replication (unmanaged)

This patch implements the unmanaged side of v2 replication in the HPE
LeftHand driver.

With unmanaged replication, the original driver instance will handle
all provisioning requests even after a failover.

cinder.conf should have the replication config group:

[lefthandrep]
hpelefthand_api_url = https://10.10.10.10:8081/lhos
hpelefthand_username = user
hpelefthand_password = pass
hpelefthand_clustername = vsa-12-5-mgmt1-vip
volume_backend_name = lefthandrep
volume_driver = cinder.volume.drivers.hpe.hpe_lefthand_iscsi.\
                HPELeftHandISCSIDriver
replication_device = target_device_id:lh-id,
                     hpelefthand_api_url:https://11.11.11.11:8081/lhos,
                     hpelefthand_username:user2,
                     hpelefthand_password:pass2,
                     hpelefthand_clustername:vsa-12-5-mgmt2-vip

Change-Id: I3c489e986648eee16b3bf5a19799a4ea0c0240b0
Implements: blueprint hp-lefthand-v2-replication
DocImpact
This commit is contained in:
Alex O'Rourke 2015-11-23 10:43:13 -08:00
parent e1349f8b61
commit aba098e575
3 changed files with 242 additions and 69 deletions

View File

@ -88,6 +88,17 @@ class HPELeftHandBaseDriver(object):
'cluster_id': 6,
'cluster_vip': '10.0.1.6'}]
repl_targets_unmgd = [{'target_device_id': 'target',
'hpelefthand_api_url': HPELEFTHAND_API_URL2,
'hpelefthand_username': HPELEFTHAND_USERNAME,
'hpelefthand_password': HPELEFTHAND_PASSWORD,
'hpelefthand_clustername': HPELEFTHAND_CLUSTER_NAME,
'hpelefthand_ssh_port': HPELEFTHAND_SSH_PORT,
'ssh_conn_timeout': HPELEFTHAND_SAN_SSH_CON_TIMEOUT,
'san_private_key': HPELEFTHAND_SAN_SSH_PRIVATE,
'cluster_id': 6,
'cluster_vip': '10.0.1.6'}]
list_rep_targets = [{'target_device_id': 'target'}]
serverName = 'fakehost'
@ -98,7 +109,8 @@ class HPELeftHandBaseDriver(object):
snapshot_id = 3
snapshot = {
'name': snapshot_name,
'volume_name': volume_name}
'volume_name': volume_name,
'volume': volume}
cloned_volume_name = "clone_volume"
cloned_volume = {'name': cloned_volume_name}
@ -1921,6 +1933,67 @@ class TestHPELeftHandISCSIDriver(HPELeftHandBaseDriver, test.TestCase):
'provider_location': prov_location},
return_model)
@mock.patch('hpelefthandclient.version', "2.0.1")
@mock.patch.object(volume_types, 'get_volume_type')
def test_create_volume_replicated_unmanaged(self, _mock_get_volume_type):
# set up driver with default config
conf = self.default_mock_conf()
conf.replication_device = self.repl_targets_unmgd
mock_client = self.setup_driver(config=conf)
mock_client.createVolume.return_value = {
'iscsiIqn': self.connector['initiator']}
mock_client.doesRemoteSnapshotScheduleExist.return_value = False
mock_replicated_client = self.setup_driver(config=conf)
_mock_get_volume_type.return_value = {
'name': 'replicated',
'extra_specs': {
'replication_enabled': '<is> True'}}
with mock.patch.object(
hpe_lefthand_iscsi.HPELeftHandISCSIDriver,
'_create_client') as mock_do_setup, \
mock.patch.object(
hpe_lefthand_iscsi.HPELeftHandISCSIDriver,
'_create_replication_client') as mock_replication_client:
mock_do_setup.return_value = mock_client
mock_replication_client.return_value = mock_replicated_client
return_model = self.driver.create_volume(self.volume_replicated)
expected = [
mock.call.createVolume(
'fakevolume_replicated',
1,
units.Gi,
{'isThinProvisioned': True,
'clusterName': 'CloudCluster1'}),
mock.call.doesRemoteSnapshotScheduleExist(
'fakevolume_replicated_SCHED_Pri'),
mock.call.createRemoteSnapshotSchedule(
'fakevolume_replicated',
'fakevolume_replicated_SCHED',
1800,
'1970-01-01T00:00:00Z',
5,
'CloudCluster1',
5,
'fakevolume_replicated',
'1.1.1.1',
'foo1',
'bar2'),
mock.call.logout()]
mock_client.assert_has_calls(
self.driver_startup_call_stack +
self.driver_startup_ssh +
expected)
prov_location = '10.0.1.6:3260,1 iqn.1993-08.org.debian:01:222 0'
rep_data = json.dumps({"location": HPELEFTHAND_API_URL})
self.assertEqual({'replication_status': 'enabled',
'replication_driver_data': rep_data,
'provider_location': prov_location},
return_model)
@mock.patch('hpelefthandclient.version', "2.0.1")
@mock.patch.object(volume_types, 'get_volume_type')
def test_delete_volume_replicated(self, _mock_get_volume_type):
@ -2203,3 +2276,50 @@ class TestHPELeftHandISCSIDriver(HPELeftHandBaseDriver, test.TestCase):
'replication_driver_data': rep_data,
'host': FAKE_FAILOVER_HOST},
return_model)
@mock.patch('hpelefthandclient.version', "2.0.1")
@mock.patch.object(volume_types, 'get_volume_type')
def test_replication_failover_unmanaged(self, _mock_get_volume_type):
ctxt = context.get_admin_context()
# set up driver with default config
conf = self.default_mock_conf()
conf.replication_device = self.repl_targets_unmgd
mock_client = self.setup_driver(config=conf)
mock_replicated_client = self.setup_driver(config=conf)
mock_replicated_client.getVolumeByName.return_value = {
'iscsiIqn': self.connector['initiator']}
_mock_get_volume_type.return_value = {
'name': 'replicated',
'extra_specs': {
'replication_enabled': '<is> True'}}
with mock.patch.object(
hpe_lefthand_iscsi.HPELeftHandISCSIDriver,
'_create_client') as mock_do_setup, \
mock.patch.object(
hpe_lefthand_iscsi.HPELeftHandISCSIDriver,
'_create_replication_client') as mock_replication_client:
mock_do_setup.return_value = mock_client
mock_replication_client.return_value = mock_replicated_client
valid_target_device_id = (self.repl_targets[0]['target_device_id'])
invalid_target_device_id = 'INVALID'
# test invalid secondary target
self.assertRaises(
exception.VolumeBackendAPIException,
self.driver.replication_failover,
ctxt,
self.volume_replicated,
invalid_target_device_id)
# test a successful failover
return_model = self.driver.replication_failover(
context.get_admin_context(),
self.volume_replicated,
valid_target_device_id)
rep_data = json.dumps({"location": HPELEFTHAND_API_URL2})
prov_location = '10.0.1.6:3260,1 iqn.1993-08.org.debian:01:222 0'
self.assertEqual({'provider_location': prov_location,
'replication_driver_data': rep_data},
return_model)

View File

@ -147,9 +147,10 @@ class HPELeftHandISCSIDriver(driver.ISCSIDriver):
2.0.0 - Rebranded HP to HPE
2.0.1 - Remove db access for consistency groups
2.0.2 - Adds v2 managed replication support
2.0.3 - Adds v2 unmanaged replication support
"""
VERSION = "2.0.2"
VERSION = "2.0.3"
device_stats = {}
@ -176,21 +177,56 @@ class HPELeftHandISCSIDriver(driver.ISCSIDriver):
# blank is the only invalid character for cluster names
# so we need to use it as a separator
self.DRIVER_LOCATION = self.__class__.__name__ + ' %(cluster)s %(vip)s'
self._client_conf = {}
self._replication_targets = []
self._replication_enabled = False
def _login(self, timeout=None):
def _login(self, volume=None, timeout=None):
conf = self._get_lefthand_config(volume)
if conf:
self._client_conf['hpelefthand_username'] = (
conf['hpelefthand_username'])
self._client_conf['hpelefthand_password'] = (
conf['hpelefthand_password'])
self._client_conf['hpelefthand_clustername'] = (
conf['hpelefthand_clustername'])
self._client_conf['hpelefthand_api_url'] = (
conf['hpelefthand_api_url'])
self._client_conf['hpelefthand_ssh_port'] = (
conf['hpelefthand_ssh_port'])
self._client_conf['hpelefthand_iscsi_chap_enabled'] = (
conf['hpelefthand_iscsi_chap_enabled'])
self._client_conf['ssh_conn_timeout'] = conf['ssh_conn_timeout']
self._client_conf['san_private_key'] = conf['san_private_key']
else:
self._client_conf['hpelefthand_username'] = (
self.configuration.hpelefthand_username)
self._client_conf['hpelefthand_password'] = (
self.configuration.hpelefthand_password)
self._client_conf['hpelefthand_clustername'] = (
self.configuration.hpelefthand_clustername)
self._client_conf['hpelefthand_api_url'] = (
self.configuration.hpelefthand_api_url)
self._client_conf['hpelefthand_ssh_port'] = (
self.configuration.hpelefthand_ssh_port)
self._client_conf['hpelefthand_iscsi_chap_enabled'] = (
self.configuration.hpelefthand_iscsi_chap_enabled)
self._client_conf['ssh_conn_timeout'] = (
self.configuration.ssh_conn_timeout)
self._client_conf['san_private_key'] = (
self.configuration.san_private_key)
client = self._create_client(timeout=timeout)
try:
if self.configuration.hpelefthand_debug:
client.debug_rest(True)
client.login(
self.configuration.hpelefthand_username,
self.configuration.hpelefthand_password)
self._client_conf['hpelefthand_username'],
self._client_conf['hpelefthand_password'])
cluster_info = client.getClusterByName(
self.configuration.hpelefthand_clustername)
self._client_conf['hpelefthand_clustername'])
self.cluster_id = cluster_info['id']
virtual_ips = cluster_info['virtualIPAddresses']
self.cluster_vip = virtual_ips[0]['ipV4Address']
@ -200,18 +236,18 @@ class HPELeftHandISCSIDriver(driver.ISCSIDriver):
if hpelefthandclient.version >= MIN_REP_CLIENT_VERSION:
# Extract IP address from API URL
ssh_ip = self._extract_ip_from_url(
self.configuration.hpelefthand_api_url)
self._client_conf['hpelefthand_api_url'])
known_hosts_file = CONF.ssh_hosts_key_file
policy = "AutoAddPolicy"
if CONF.strict_ssh_host_key_policy:
policy = "RejectPolicy"
client.setSSHOptions(
ssh_ip,
self.configuration.hpelefthand_username,
self.configuration.hpelefthand_password,
port=self.configuration.hpelefthand_ssh_port,
conn_timeout=self.configuration.ssh_conn_timeout,
privatekey=self.configuration.san_private_key,
self._client_conf['hpelefthand_username'],
self._client_conf['hpelefthand_password'],
port=self._client_conf['hpelefthand_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)
@ -229,12 +265,12 @@ class HPELeftHandISCSIDriver(driver.ISCSIDriver):
def _create_client(self, timeout=None):
# Timeout is only supported in version 2.0.1 and greater of the
# python-lefthandclient.
hpelefthand_api_url = self._client_conf['hpelefthand_api_url']
if hpelefthandclient.version >= MIN_REP_CLIENT_VERSION:
client = hpe_lh_client.HPELeftHandClient(
self.configuration.hpelefthand_api_url, timeout=timeout)
hpelefthand_api_url, timeout=timeout)
else:
client = hpe_lh_client.HPELeftHandClient(
self.configuration.hpelefthand_api_url)
client = hpe_lh_client.HPELeftHandClient(hpelefthand_api_url)
return client
def _create_replication_client(self, remote_array):
@ -312,6 +348,14 @@ class HPELeftHandISCSIDriver(driver.ISCSIDriver):
finally:
self._logout(client)
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 get_version_string(self):
return (_('REST %(proxy_ver)s hpelefthandclient %(rest_ver)s') % {
'proxy_ver': self.VERSION,
@ -319,7 +363,7 @@ class HPELeftHandISCSIDriver(driver.ISCSIDriver):
def create_volume(self, volume):
"""Creates a volume."""
client = self._login()
client = self._login(volume)
try:
# get the extra specs of interest from this volume's volume type
volume_extra_specs = self._get_volume_extra_specs(volume)
@ -344,7 +388,7 @@ class HPELeftHandISCSIDriver(driver.ISCSIDriver):
if optional.get('isAdaptiveOptimizationEnabled'):
del optional['isAdaptiveOptimizationEnabled']
clusterName = self.configuration.hpelefthand_clustername
clusterName = self._client_conf['hpelefthand_clustername']
optional['clusterName'] = clusterName
volume_info = client.createVolume(
@ -359,7 +403,7 @@ class HPELeftHandISCSIDriver(driver.ISCSIDriver):
self._do_volume_replication_setup(volume, client, optional)):
model_update['replication_status'] = 'enabled'
model_update['replication_driver_data'] = (json.dumps(
{'location': self.configuration.hpelefthand_api_url}))
{'location': self._client_conf['hpelefthand_api_url']}))
return model_update
except Exception as ex:
@ -369,7 +413,7 @@ class HPELeftHandISCSIDriver(driver.ISCSIDriver):
def delete_volume(self, volume):
"""Deletes a volume."""
client = self._login()
client = self._login(volume)
# v2 replication check
# If the volume type is replication enabled, we want to call our own
# method of deconstructing the volume and its dependencies
@ -389,7 +433,7 @@ class HPELeftHandISCSIDriver(driver.ISCSIDriver):
def extend_volume(self, volume, new_size):
"""Extend the size of an existing volume."""
client = self._login()
client = self._login(volume)
try:
volume_info = client.getVolumeByName(volume['name'])
@ -538,7 +582,7 @@ class HPELeftHandISCSIDriver(driver.ISCSIDriver):
def create_snapshot(self, snapshot):
"""Creates a snapshot."""
client = self._login()
client = self._login(snapshot['volume'])
try:
volume_info = client.getVolumeByName(snapshot['volume_name'])
@ -553,7 +597,7 @@ class HPELeftHandISCSIDriver(driver.ISCSIDriver):
def delete_snapshot(self, snapshot):
"""Deletes a snapshot."""
client = self._login()
client = self._login(snapshot['volume'])
try:
snap_info = client.getSnapshotByName(snapshot['name'])
client.deleteSnapshot(snap_info['id'])
@ -592,7 +636,7 @@ class HPELeftHandISCSIDriver(driver.ISCSIDriver):
data['storage_protocol'] = 'iSCSI'
data['vendor_name'] = 'Hewlett Packard Enterprise'
data['location_info'] = (self.DRIVER_LOCATION % {
'cluster': self.configuration.hpelefthand_clustername,
'cluster': self._client_conf['hpelefthand_clustername'],
'vip': self.cluster_vip})
data['thin_provisioning_support'] = True
data['thick_provisioning_support'] = True
@ -618,7 +662,7 @@ class HPELeftHandISCSIDriver(driver.ISCSIDriver):
total_volumes = 0
provisioned_size = 0
volumes = client.getVolumes(
cluster=self.configuration.hpelefthand_clustername,
cluster=self._client_conf['hpelefthand_clustername'],
fields=['members[id]', 'members[clusterName]', 'members[size]'])
if volumes:
total_volumes = volumes['total']
@ -645,7 +689,7 @@ class HPELeftHandISCSIDriver(driver.ISCSIDriver):
used from that host. HPE VSA requires a volume to be assigned
to a server.
"""
client = self._login()
client = self._login(volume)
try:
server_info = self._create_server(connector, client)
volume_info = client.getVolumeByName(volume['name'])
@ -682,7 +726,7 @@ class HPELeftHandISCSIDriver(driver.ISCSIDriver):
def terminate_connection(self, volume, connector, **kwargs):
"""Unassign the volume from the host."""
client = self._login()
client = self._login(volume)
try:
volume_info = client.getVolumeByName(volume['name'])
server_info = client.getServerByName(connector['host'])
@ -707,7 +751,7 @@ class HPELeftHandISCSIDriver(driver.ISCSIDriver):
def create_volume_from_snapshot(self, volume, snapshot):
"""Creates a volume from a snapshot."""
client = self._login()
client = self._login(volume)
try:
snap_info = client.getSnapshotByName(snapshot['name'])
volume_info = client.cloneSnapshot(
@ -721,7 +765,7 @@ class HPELeftHandISCSIDriver(driver.ISCSIDriver):
self._do_volume_replication_setup(volume, client)):
model_update['replication_status'] = 'enabled'
model_update['replication_driver_data'] = (json.dumps(
{'location': self.configuration.hpelefthand_api_url}))
{'location': self._client_conf['hpelefthand_api_url']}))
return model_update
except Exception as ex:
@ -730,7 +774,7 @@ class HPELeftHandISCSIDriver(driver.ISCSIDriver):
self._logout(client)
def create_cloned_volume(self, volume, src_vref):
client = self._login()
client = self._login(volume)
try:
volume_info = client.getVolumeByName(src_vref['name'])
clone_info = client.cloneVolume(volume['name'], volume_info['id'])
@ -742,7 +786,7 @@ class HPELeftHandISCSIDriver(driver.ISCSIDriver):
self._do_volume_replication_setup(volume, client)):
model_update['replication_status'] = 'enabled'
model_update['replication_driver_data'] = (json.dumps(
{'location': self.configuration.hpelefthand_api_url}))
{'location': self._client_conf['hpelefthand_api_url']}))
return model_update
except Exception as ex:
@ -802,7 +846,7 @@ class HPELeftHandISCSIDriver(driver.ISCSIDriver):
def _create_server(self, connector, client):
server_info = None
chap_enabled = self.configuration.hpelefthand_iscsi_chap_enabled
chap_enabled = self._client_conf['hpelefthand_iscsi_chap_enabled']
try:
server_info = client.getServerByName(connector['host'])
chap_secret = server_info['chapTargetSecret']
@ -857,7 +901,7 @@ class HPELeftHandISCSIDriver(driver.ISCSIDriver):
'new_type': new_type,
'diff': diff,
'host': host})
client = self._login()
client = self._login(volume)
try:
volume_info = client.getVolumeByName(volume['name'])
@ -911,19 +955,18 @@ class HPELeftHandISCSIDriver(driver.ISCSIDriver):
host['host'] is its name, and host['capabilities'] is a
dictionary of its reported capabilities.
"""
LOG.debug('enter: migrate_volume: id=%(id)s, host=%(host)s, '
'cluster=%(cluster)s', {
'id': volume['id'],
'host': host,
'cluster': self.configuration.hpelefthand_clustername})
false_ret = (False, None)
if 'location_info' not in host['capabilities']:
return false_ret
host_location = host['capabilities']['location_info']
(driver, cluster, vip) = host_location.split(' ')
client = self._login()
client = self._login(volume)
LOG.debug('enter: migrate_volume: id=%(id)s, host=%(host)s, '
'cluster=%(cluster)s', {
'id': volume['id'],
'host': host,
'cluster': self._client_conf['hpelefthand_clustername']})
try:
# get the cluster info, if it exists and compare
cluster_info = client.getClusterByName(cluster)
@ -1003,7 +1046,7 @@ class HPELeftHandISCSIDriver(driver.ISCSIDriver):
# volume isn't attached and can be updated
original_name = CONF.volume_name_template % volume['id']
current_name = CONF.volume_name_template % new_volume['id']
client = self._login()
client = self._login(volume)
try:
volume_info = client.getVolumeByName(current_name)
volumeMods = {'name': original_name}
@ -1038,7 +1081,7 @@ class HPELeftHandISCSIDriver(driver.ISCSIDriver):
target_vol_name = self._get_existing_volume_ref_name(existing_ref)
# Check for the existence of the virtual volume.
client = self._login()
client = self._login(volume)
try:
volume_info = client.getVolumeByName(target_vol_name)
except hpeexceptions.HTTPNotFound:
@ -1136,7 +1179,7 @@ class HPELeftHandISCSIDriver(driver.ISCSIDriver):
reason=reason)
# Check for the existence of the virtual volume.
client = self._login()
client = self._login(volume)
try:
volume_info = client.getVolumeByName(target_vol_name)
except hpeexceptions.HTTPNotFound:
@ -1156,7 +1199,7 @@ class HPELeftHandISCSIDriver(driver.ISCSIDriver):
# Rename the volume's name to unm-* format so that it can be
# easily found later.
client = self._login()
client = self._login(volume)
try:
volume_info = client.getVolumeByName(volume['name'])
new_vol_name = 'unm-' + six.text_type(volume['id'])
@ -1218,7 +1261,7 @@ class HPELeftHandISCSIDriver(driver.ISCSIDriver):
LOG.error(msg)
model_update['replication_status'] = "error"
else:
client = self._login()
client = self._login(volume)
try:
if self._do_volume_replication_setup(volume, client):
model_update['replication_status'] = "enabled"
@ -1238,7 +1281,7 @@ class HPELeftHandISCSIDriver(driver.ISCSIDriver):
model_update['replication_status'] = 'disabled'
vol_name = volume['name']
client = self._login()
client = self._login(volume)
try:
name = vol_name + self.REP_SCHEDULE_SUFFIX + "_Pri"
client.stopRemoteSnapshotSchedule(name)
@ -1354,32 +1397,19 @@ class HPELeftHandISCSIDriver(driver.ISCSIDriver):
# as a failover can still occur, so we need out replication
# devices to exist.
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['target_device_id'] = (
dev.get('target_device_id'))
remote_array['hpelefthand_api_url'] = (
dev.get('hpelefthand_api_url'))
remote_array['hpelefthand_username'] = (
dev.get('hpelefthand_username'))
remote_array['hpelefthand_password'] = (
dev.get('hpelefthand_password'))
remote_array['hpelefthand_clustername'] = (
dev.get('hpelefthand_clustername'))
remote_array = dict(dev.items())
# Override and set defaults for certain entries
remote_array['managed_backend_name'] = (
dev.get('managed_backend_name'))
remote_array['hpelefthand_ssh_port'] = (
dev.get('hpelefthand_ssh_port', default_san_ssh_port))
remote_array['ssh_conn_timeout'] = (
dev.get('ssh_conn_timeout', default_ssh_conn_timeout))
remote_array['san_private_key'] = (
dev.get('san_private_key', default_san_private_key))
# Format hpe3par_iscsi_chap_enabled as a bool
remote_array['hpelefthand_iscsi_chap_enabled'] = (
dev.get('hpelefthand_iscsi_chap_enabled') == 'True')
remote_array['cluster_id'] = None
remote_array['cluster_vip'] = None
array_name = remote_array['target_device_id']
@ -1432,10 +1462,14 @@ class HPELeftHandISCSIDriver(driver.ISCSIDriver):
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 = ['hpelefthand_api_url', 'hpelefthand_username',
'hpelefthand_password', 'target_device_id',
'hpelefthand_clustername']
try:
self.check_replication_flags(target, required_flags)
return True
except Exception:
return False
def _is_replication_configured_correct(self):
rep_flag = True
@ -1466,6 +1500,22 @@ class HPELeftHandISCSIDriver(driver.ISCSIDriver):
exists = False
return exists
def _get_lefthand_config(self, volume):
conf = None
if volume:
rep_location = None
rep_data = volume.get('replication_driver_data')
if rep_data:
rep_data = json.loads(rep_data)
rep_location = rep_data.get('location')
if rep_location:
for target in self._replication_targets:
if target['hpelefthand_api_url'] == rep_location:
conf = target
break
return conf
def _do_volume_replication_setup(self, volume, client, optional=None):
"""This function will do or ensure the following:

View File

@ -0,0 +1,3 @@
---
features:
- Added unmanaged v2 replication support to the HPE LeftHand driver.