Merge "LeftHand: Implement v2 replication (managed)"
This commit is contained in:
commit
f1c692b8fc
cinder
releasenotes/notes
@ -1,4 +1,4 @@
|
||||
# (c) Copyright 2014-2015 Hewlett Packard Enterprise Development LP
|
||||
# (c) Copyright 2014-2016 Hewlett Packard Enterprise Development LP
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
@ -15,6 +15,7 @@
|
||||
#
|
||||
"""Unit tests for OpenStack Cinder volume drivers."""
|
||||
|
||||
import json
|
||||
import mock
|
||||
from oslo_utils import units
|
||||
|
||||
@ -32,6 +33,18 @@ GOODNESS_FUNCTION = \
|
||||
"capabilities.capacity_utilization < 0.6? 100 : 25"
|
||||
FILTER_FUNCTION = \
|
||||
"capabilities.total_volumes < 400 && capabilities.capacity_utilization"
|
||||
HPELEFTHAND_SAN_SSH_CON_TIMEOUT = 44
|
||||
HPELEFTHAND_SAN_SSH_PRIVATE = 'foobar'
|
||||
HPELEFTHAND_API_URL = 'http://fake.foo:8080/lhos'
|
||||
HPELEFTHAND_API_URL2 = 'http://fake2.foo2:8080/lhos'
|
||||
HPELEFTHAND_SSH_IP = 'fake.foo'
|
||||
HPELEFTHAND_SSH_IP2 = 'fake2.foo2'
|
||||
HPELEFTHAND_USERNAME = 'foo1'
|
||||
HPELEFTHAND_PASSWORD = 'bar2'
|
||||
HPELEFTHAND_SSH_PORT = 16022
|
||||
HPELEFTHAND_CLUSTER_NAME = 'CloudCluster1'
|
||||
VOLUME_TYPE_ID_REPLICATED = 'be9181f1-4040-46f2-8298-e7532f2bf9db'
|
||||
FAKE_FAILOVER_HOST = 'fakefailover@foo#destfakepool'
|
||||
|
||||
|
||||
class HPELeftHandBaseDriver(object):
|
||||
@ -39,6 +52,7 @@ class HPELeftHandBaseDriver(object):
|
||||
cluster_id = 1
|
||||
|
||||
volume_name = "fakevolume"
|
||||
volume_name_repl = "fakevolume_replicated"
|
||||
volume_id = 1
|
||||
volume = {
|
||||
'name': volume_name,
|
||||
@ -49,6 +63,33 @@ class HPELeftHandBaseDriver(object):
|
||||
'provider_auth': None,
|
||||
'size': 1}
|
||||
|
||||
volume_replicated = {
|
||||
'name': volume_name_repl,
|
||||
'display_name': 'Foo Volume',
|
||||
'provider_location': ('10.0.1.6 iqn.2003-10.com.lefthandnetworks:'
|
||||
'group01:25366:fakev 0'),
|
||||
'id': volume_id,
|
||||
'provider_auth': None,
|
||||
'size': 1,
|
||||
'volume_type': 'replicated',
|
||||
'volume_type_id': VOLUME_TYPE_ID_REPLICATED,
|
||||
'replication_driver_data': ('{"location": "' + HPELEFTHAND_API_URL +
|
||||
'"}')}
|
||||
|
||||
repl_targets = [{'target_device_id': 'target',
|
||||
'managed_backend_name': FAKE_FAILOVER_HOST,
|
||||
'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'
|
||||
server_id = 0
|
||||
server_uri = '/lhos/servers/0'
|
||||
@ -98,6 +139,18 @@ class HPELeftHandBaseDriver(object):
|
||||
mock.call.getClusterByName('CloudCluster1'),
|
||||
]
|
||||
|
||||
driver_startup_ssh = [
|
||||
mock.call.setSSHOptions(
|
||||
HPELEFTHAND_SSH_IP,
|
||||
HPELEFTHAND_USERNAME,
|
||||
HPELEFTHAND_PASSWORD,
|
||||
missing_key_policy='AutoAddPolicy',
|
||||
privatekey=HPELEFTHAND_SAN_SSH_PRIVATE,
|
||||
known_hosts_file=mock.ANY,
|
||||
port=HPELEFTHAND_SSH_PORT,
|
||||
conn_timeout=HPELEFTHAND_SAN_SSH_CON_TIMEOUT),
|
||||
]
|
||||
|
||||
|
||||
class TestHPELeftHandISCSIDriver(HPELeftHandBaseDriver, test.TestCase):
|
||||
|
||||
@ -119,10 +172,13 @@ class TestHPELeftHandISCSIDriver(HPELeftHandBaseDriver, test.TestCase):
|
||||
|
||||
def default_mock_conf(self):
|
||||
|
||||
mock_conf = mock.Mock()
|
||||
mock_conf.hpelefthand_api_url = 'http://fake.foo:8080/lhos'
|
||||
mock_conf.hpelefthand_username = 'foo1'
|
||||
mock_conf.hpelefthand_password = 'bar2'
|
||||
mock_conf = mock.MagicMock()
|
||||
mock_conf.hpelefthand_api_url = HPELEFTHAND_API_URL
|
||||
mock_conf.hpelefthand_username = HPELEFTHAND_USERNAME
|
||||
mock_conf.hpelefthand_password = HPELEFTHAND_PASSWORD
|
||||
mock_conf.hpelefthand_ssh_port = HPELEFTHAND_SSH_PORT
|
||||
mock_conf.ssh_conn_timeout = HPELEFTHAND_SAN_SSH_CON_TIMEOUT
|
||||
mock_conf.san_private_key = HPELEFTHAND_SAN_SSH_PRIVATE
|
||||
mock_conf.hpelefthand_iscsi_chap_enabled = False
|
||||
mock_conf.hpelefthand_debug = False
|
||||
mock_conf.hpelefthand_clustername = "CloudCluster1"
|
||||
@ -149,6 +205,8 @@ class TestHPELeftHandISCSIDriver(HPELeftHandBaseDriver, test.TestCase):
|
||||
_mock_client.return_value.getCluster.return_value = {
|
||||
'spaceTotal': units.Gi * 500,
|
||||
'spaceAvailable': units.Gi * 250}
|
||||
_mock_client.return_value.getApiVersion.return_value = '1.2'
|
||||
_mock_client.return_value.getIPFromCluster.return_value = '1.1.1.1'
|
||||
self.driver = hpe_lefthand_iscsi.HPELeftHandISCSIDriver(
|
||||
configuration=config)
|
||||
self.driver.do_setup(None)
|
||||
@ -305,7 +363,9 @@ class TestHPELeftHandISCSIDriver(HPELeftHandBaseDriver, test.TestCase):
|
||||
mock_do_setup.return_value = mock_client
|
||||
|
||||
# execute delete_volume
|
||||
self.driver.delete_volume(self.volume)
|
||||
del_volume = self.volume
|
||||
del_volume['volume_type_id'] = None
|
||||
self.driver.delete_volume(del_volume)
|
||||
|
||||
expected = self.driver_startup_call_stack + [
|
||||
mock.call.getVolumeByName('fakevolume'),
|
||||
@ -318,13 +378,13 @@ class TestHPELeftHandISCSIDriver(HPELeftHandBaseDriver, test.TestCase):
|
||||
mock_client.getVolumeByName.side_effect =\
|
||||
hpeexceptions.HTTPNotFound()
|
||||
# no exception should escape method
|
||||
self.driver.delete_volume(self.volume)
|
||||
self.driver.delete_volume(del_volume)
|
||||
|
||||
# mock HTTPConflict
|
||||
mock_client.deleteVolume.side_effect = hpeexceptions.HTTPConflict()
|
||||
# ensure the raised exception is a cinder exception
|
||||
self.assertRaises(exception.VolumeBackendAPIException,
|
||||
self.driver.delete_volume, self.volume_id)
|
||||
self.driver.delete_volume, {})
|
||||
|
||||
def test_extend_volume(self):
|
||||
|
||||
@ -1799,3 +1859,347 @@ class TestHPELeftHandISCSIDriver(HPELeftHandBaseDriver, test.TestCase):
|
||||
cgsnap, snaps = self.driver.delete_cgsnapshot(
|
||||
ctxt, cgsnapshot, expected_snaps)
|
||||
self.assertEqual('deleting', cgsnap['status'])
|
||||
|
||||
@mock.patch('hpelefthandclient.version', "2.0.1")
|
||||
@mock.patch.object(volume_types, 'get_volume_type')
|
||||
def test_create_volume_replicated_managed(self, _mock_get_volume_type):
|
||||
# set up driver with default config
|
||||
conf = self.default_mock_conf()
|
||||
conf.replication_device = self.repl_targets
|
||||
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):
|
||||
# set up driver with default config
|
||||
conf = self.default_mock_conf()
|
||||
conf.replication_device = self.repl_targets
|
||||
mock_client = self.setup_driver(config=conf)
|
||||
mock_client.getVolumeByName.return_value = {'id': self.volume_id}
|
||||
mock_client.getVolumes.return_value = {'total': 1, 'members': []}
|
||||
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
|
||||
self.driver.delete_volume(self.volume_replicated)
|
||||
|
||||
expected = [
|
||||
mock.call.deleteRemoteSnapshotSchedule(
|
||||
'fakevolume_replicated_SCHED'),
|
||||
mock.call.getVolumeByName('fakevolume_replicated'),
|
||||
mock.call.deleteVolume(1)]
|
||||
mock_client.assert_has_calls(
|
||||
self.driver_startup_call_stack +
|
||||
self.driver_startup_ssh +
|
||||
expected)
|
||||
|
||||
@mock.patch('hpelefthandclient.version', "2.0.1")
|
||||
@mock.patch.object(volume_types, 'get_volume_type')
|
||||
def test_replication_enable_no_snapshot_schedule(self,
|
||||
_mock_get_volume_type):
|
||||
# set up driver with default config
|
||||
conf = self.default_mock_conf()
|
||||
conf.replication_device = self.repl_targets
|
||||
mock_client = self.setup_driver(config=conf)
|
||||
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.replication_enable(
|
||||
context.get_admin_context(),
|
||||
self.volume_replicated)
|
||||
|
||||
expected = [
|
||||
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_client.assert_has_calls(
|
||||
self.driver_startup_call_stack +
|
||||
self.driver_startup_ssh +
|
||||
expected)
|
||||
|
||||
self.assertEqual({'replication_status': 'enabled'},
|
||||
return_model)
|
||||
|
||||
@mock.patch('hpelefthandclient.version', "2.0.1")
|
||||
@mock.patch.object(volume_types, 'get_volume_type')
|
||||
def test_replication_enable_with_snapshot_schedule(self,
|
||||
_mock_get_volume_type):
|
||||
# set up driver with default config
|
||||
conf = self.default_mock_conf()
|
||||
conf.replication_device = self.repl_targets
|
||||
mock_client = self.setup_driver(config=conf)
|
||||
mock_client.doesRemoteSnapshotScheduleExist.return_value = True
|
||||
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.replication_enable(
|
||||
context.get_admin_context(),
|
||||
self.volume_replicated)
|
||||
|
||||
expected = [
|
||||
mock.call.doesRemoteSnapshotScheduleExist(
|
||||
'fakevolume_replicated_SCHED_Pri'),
|
||||
mock.call.startRemoteSnapshotSchedule(
|
||||
'fakevolume_replicated_SCHED_Pri')]
|
||||
mock_client.assert_has_calls(
|
||||
self.driver_startup_call_stack +
|
||||
self.driver_startup_ssh +
|
||||
expected)
|
||||
|
||||
self.assertEqual({'replication_status': 'enabled'},
|
||||
return_model)
|
||||
|
||||
@mock.patch('hpelefthandclient.version', "2.0.1")
|
||||
@mock.patch.object(volume_types, 'get_volume_type')
|
||||
def test_replication_disable(self, _mock_get_volume_type):
|
||||
# set up driver with default config
|
||||
conf = self.default_mock_conf()
|
||||
conf.replication_device = self.repl_targets
|
||||
mock_client = self.setup_driver(config=conf)
|
||||
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.replication_disable(
|
||||
context.get_admin_context(),
|
||||
self.volume_replicated)
|
||||
|
||||
expected = [
|
||||
mock.call.stopRemoteSnapshotSchedule(
|
||||
'fakevolume_replicated_SCHED_Pri')]
|
||||
mock_client.assert_has_calls(
|
||||
self.driver_startup_call_stack +
|
||||
self.driver_startup_ssh +
|
||||
expected)
|
||||
|
||||
self.assertEqual({'replication_status': 'disabled'},
|
||||
return_model)
|
||||
|
||||
@mock.patch('hpelefthandclient.version', "2.0.1")
|
||||
@mock.patch.object(volume_types, 'get_volume_type')
|
||||
def test_replication_disable_fail(self, _mock_get_volume_type):
|
||||
# set up driver with default config
|
||||
conf = self.default_mock_conf()
|
||||
conf.replication_device = self.repl_targets
|
||||
mock_client = self.setup_driver(config=conf)
|
||||
mock_client.stopRemoteSnapshotSchedule.side_effect = (
|
||||
Exception("Error: Could not stop remote snapshot schedule."))
|
||||
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.replication_disable(
|
||||
context.get_admin_context(),
|
||||
self.volume_replicated)
|
||||
|
||||
expected = [
|
||||
mock.call.stopRemoteSnapshotSchedule(
|
||||
'fakevolume_replicated_SCHED_Pri')]
|
||||
mock_client.assert_has_calls(
|
||||
self.driver_startup_call_stack +
|
||||
self.driver_startup_ssh +
|
||||
expected)
|
||||
|
||||
self.assertEqual({'replication_status': 'disable_failed'},
|
||||
return_model)
|
||||
|
||||
@mock.patch('hpelefthandclient.version', "2.0.1")
|
||||
@mock.patch.object(volume_types, 'get_volume_type')
|
||||
def test_list_replication_targets(self, _mock_get_volume_type):
|
||||
# set up driver with default config
|
||||
conf = self.default_mock_conf()
|
||||
conf.replication_device = self.repl_targets
|
||||
mock_client = self.setup_driver(config=conf)
|
||||
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.list_replication_targets(
|
||||
context.get_admin_context(),
|
||||
self.volume_replicated)
|
||||
|
||||
targets = self.list_rep_targets
|
||||
self.assertEqual({'volume_id': 1,
|
||||
'targets': targets},
|
||||
return_model)
|
||||
|
||||
@mock.patch('hpelefthandclient.version', "2.0.1")
|
||||
@mock.patch.object(volume_types, 'get_volume_type')
|
||||
def test_replication_failover_managed(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
|
||||
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,
|
||||
'host': FAKE_FAILOVER_HOST},
|
||||
return_model)
|
||||
|
@ -1,4 +1,4 @@
|
||||
# (c) Copyright 2014-2015 Hewlett Packard Enterprise Development LP
|
||||
# (c) Copyright 2014-2016 Hewlett Packard Enterprise Development LP
|
||||
# All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||
@ -36,6 +36,7 @@ LeftHand array.
|
||||
|
||||
from oslo_config import cfg
|
||||
from oslo_log import log as logging
|
||||
from oslo_serialization import jsonutils as json
|
||||
from oslo_utils import excutils
|
||||
from oslo_utils import importutils
|
||||
from oslo_utils import units
|
||||
@ -45,13 +46,13 @@ from cinder import exception
|
||||
from cinder.i18n import _, _LE, _LI, _LW
|
||||
from cinder.objects import fields
|
||||
from cinder.volume import driver
|
||||
from cinder.volume.drivers.san import san
|
||||
from cinder.volume import utils
|
||||
from cinder.volume import volume_types
|
||||
|
||||
import six
|
||||
|
||||
import math
|
||||
import re
|
||||
import six
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
|
||||
@ -88,6 +89,9 @@ hpelefthand_opts = [
|
||||
default=False,
|
||||
help="Enable HTTP debugging to LeftHand",
|
||||
deprecated_name='hplefthand_debug'),
|
||||
cfg.PortOpt('hpelefthand_ssh_port',
|
||||
default=16022,
|
||||
help="Port number of SSH service."),
|
||||
|
||||
]
|
||||
|
||||
@ -96,6 +100,7 @@ CONF.register_opts(hpelefthand_opts)
|
||||
|
||||
MIN_API_VERSION = "1.1"
|
||||
MIN_CLIENT_VERSION = '2.0.0'
|
||||
MIN_REP_CLIENT_VERSION = '2.0.1'
|
||||
|
||||
# map the extra spec key to the REST client option key
|
||||
extra_specs_key_map = {
|
||||
@ -141,24 +146,41 @@ class HPELeftHandISCSIDriver(driver.ISCSIDriver):
|
||||
1.0.14 - Removed the old CLIQ based driver
|
||||
2.0.0 - Rebranded HP to HPE
|
||||
2.0.1 - Remove db access for consistency groups
|
||||
2.0.2 - Adds v2 managed replication support
|
||||
"""
|
||||
|
||||
VERSION = "2.0.1"
|
||||
VERSION = "2.0.2"
|
||||
|
||||
device_stats = {}
|
||||
|
||||
# v2 replication constants
|
||||
EXTRA_SPEC_REP_SYNC_PERIOD = "replication:sync_period"
|
||||
EXTRA_SPEC_REP_RETENTION_COUNT = "replication:retention_count"
|
||||
EXTRA_SPEC_REP_REMOTE_RETENTION_COUNT = (
|
||||
"replication:remote_retention_count")
|
||||
MIN_REP_SYNC_PERIOD = 1800
|
||||
DEFAULT_RETENTION_COUNT = 5
|
||||
MAX_RETENTION_COUNT = 50
|
||||
DEFAULT_REMOTE_RETENTION_COUNT = 5
|
||||
MAX_REMOTE_RETENTION_COUNT = 50
|
||||
REP_SNAPSHOT_SUFFIX = "_SS"
|
||||
REP_SCHEDULE_SUFFIX = "_SCHED"
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(HPELeftHandISCSIDriver, self).__init__(*args, **kwargs)
|
||||
self.configuration.append_config_values(hpelefthand_opts)
|
||||
self.configuration.append_config_values(san.san_opts)
|
||||
if not self.configuration.hpelefthand_api_url:
|
||||
raise exception.NotFound(_("HPELeftHand url not found"))
|
||||
|
||||
# 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._replication_targets = []
|
||||
self._replication_enabled = False
|
||||
|
||||
def _login(self):
|
||||
client = self._create_client()
|
||||
def _login(self, timeout=None):
|
||||
client = self._create_client(timeout=timeout)
|
||||
try:
|
||||
if self.configuration.hpelefthand_debug:
|
||||
client.debug_rest(True)
|
||||
@ -173,6 +195,26 @@ class HPELeftHandISCSIDriver(driver.ISCSIDriver):
|
||||
virtual_ips = cluster_info['virtualIPAddresses']
|
||||
self.cluster_vip = virtual_ips[0]['ipV4Address']
|
||||
|
||||
# SSH is only available in the 2.0.1 release of the
|
||||
# python-lefthandclient.
|
||||
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)
|
||||
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,
|
||||
missing_key_policy=policy,
|
||||
known_hosts_file=known_hosts_file)
|
||||
|
||||
return client
|
||||
except hpeexceptions.HTTPNotFound:
|
||||
raise exception.DriverNotInitialized(
|
||||
@ -181,11 +223,60 @@ class HPELeftHandISCSIDriver(driver.ISCSIDriver):
|
||||
raise exception.DriverNotInitialized(ex)
|
||||
|
||||
def _logout(self, client):
|
||||
client.logout()
|
||||
if client is not None:
|
||||
client.logout()
|
||||
|
||||
def _create_client(self):
|
||||
return hpe_lh_client.HPELeftHandClient(
|
||||
self.configuration.hpelefthand_api_url)
|
||||
def _create_client(self, timeout=None):
|
||||
# Timeout is only supported in version 2.0.1 and greater of the
|
||||
# python-lefthandclient.
|
||||
if hpelefthandclient.version >= MIN_REP_CLIENT_VERSION:
|
||||
client = hpe_lh_client.HPELeftHandClient(
|
||||
self.configuration.hpelefthand_api_url, timeout=timeout)
|
||||
else:
|
||||
client = hpe_lh_client.HPELeftHandClient(
|
||||
self.configuration.hpelefthand_api_url)
|
||||
return client
|
||||
|
||||
def _create_replication_client(self, remote_array):
|
||||
cl = hpe_lh_client.HPELeftHandClient(
|
||||
remote_array['hpelefthand_api_url'])
|
||||
try:
|
||||
cl.login(
|
||||
remote_array['hpelefthand_username'],
|
||||
remote_array['hpelefthand_password'])
|
||||
|
||||
# Extract IP address from API URL
|
||||
ssh_ip = self._extract_ip_from_url(
|
||||
remote_array['hpelefthand_api_url'])
|
||||
known_hosts_file = CONF.ssh_hosts_key_file
|
||||
policy = "AutoAddPolicy"
|
||||
if CONF.strict_ssh_host_key_policy:
|
||||
policy = "RejectPolicy"
|
||||
cl.setSSHOptions(
|
||||
ssh_ip,
|
||||
remote_array['hpelefthand_username'],
|
||||
remote_array['hpelefthand_password'],
|
||||
port=remote_array['hpelefthand_ssh_port'],
|
||||
conn_timeout=remote_array['ssh_conn_timeout'],
|
||||
privatekey=remote_array['san_private_key'],
|
||||
missing_key_policy=policy,
|
||||
known_hosts_file=known_hosts_file)
|
||||
|
||||
return cl
|
||||
except hpeexceptions.HTTPNotFound:
|
||||
raise exception.DriverNotInitialized(
|
||||
_('LeftHand cluster not found'))
|
||||
except Exception as ex:
|
||||
raise exception.DriverNotInitialized(ex)
|
||||
|
||||
def _destroy_replication_client(self, client):
|
||||
if client is not None:
|
||||
client.logout()
|
||||
|
||||
def _extract_ip_from_url(self, url):
|
||||
result = re.search("://(.*):", url)
|
||||
ip = result.group(1)
|
||||
return ip
|
||||
|
||||
def do_setup(self, context):
|
||||
"""Set up LeftHand client."""
|
||||
@ -200,6 +291,10 @@ class HPELeftHandISCSIDriver(driver.ISCSIDriver):
|
||||
LOG.error(ex_msg)
|
||||
raise exception.InvalidInput(reason=ex_msg)
|
||||
|
||||
# v2 replication check
|
||||
if hpelefthandclient.version >= MIN_REP_CLIENT_VERSION:
|
||||
self._do_replication_setup()
|
||||
|
||||
def check_for_setup_error(self):
|
||||
"""Checks for incorrect LeftHand API being used on backend."""
|
||||
client = self._login()
|
||||
@ -257,7 +352,16 @@ class HPELeftHandISCSIDriver(driver.ISCSIDriver):
|
||||
volume['size'] * units.Gi,
|
||||
optional)
|
||||
|
||||
return self._update_provider(volume_info)
|
||||
model_update = self._update_provider(volume_info)
|
||||
|
||||
# v2 replication check
|
||||
if self._volume_of_replicated_type(volume) and (
|
||||
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}))
|
||||
|
||||
return model_update
|
||||
except Exception as ex:
|
||||
raise exception.VolumeBackendAPIException(data=ex)
|
||||
finally:
|
||||
@ -266,6 +370,13 @@ class HPELeftHandISCSIDriver(driver.ISCSIDriver):
|
||||
def delete_volume(self, volume):
|
||||
"""Deletes a volume."""
|
||||
client = self._login()
|
||||
# v2 replication check
|
||||
# If the volume type is replication enabled, we want to call our own
|
||||
# method of deconstructing the volume and its dependencies
|
||||
if self._volume_of_replicated_type(volume):
|
||||
self._do_volume_replication_destroy(volume, client)
|
||||
return
|
||||
|
||||
try:
|
||||
volume_info = client.getVolumeByName(volume['name'])
|
||||
client.deleteVolume(volume_info['id'])
|
||||
@ -520,6 +631,11 @@ class HPELeftHandISCSIDriver(driver.ISCSIDriver):
|
||||
data['goodness_function'] = self.get_goodness_function()
|
||||
data['consistencygroup_support'] = True
|
||||
|
||||
if hpelefthandclient.version >= MIN_REP_CLIENT_VERSION:
|
||||
data['replication_enabled'] = self._replication_enabled
|
||||
data['replication_type'] = ['periodic']
|
||||
data['replication_count'] = len(self._replication_targets)
|
||||
|
||||
self.device_stats = data
|
||||
|
||||
def initialize_connection(self, volume, connector):
|
||||
@ -597,7 +713,17 @@ class HPELeftHandISCSIDriver(driver.ISCSIDriver):
|
||||
volume_info = client.cloneSnapshot(
|
||||
volume['name'],
|
||||
snap_info['id'])
|
||||
return self._update_provider(volume_info)
|
||||
|
||||
model_update = self._update_provider(volume_info)
|
||||
|
||||
# v2 replication check
|
||||
if self._volume_of_replicated_type(volume) and (
|
||||
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}))
|
||||
|
||||
return model_update
|
||||
except Exception as ex:
|
||||
raise exception.VolumeBackendAPIException(ex)
|
||||
finally:
|
||||
@ -608,7 +734,17 @@ class HPELeftHandISCSIDriver(driver.ISCSIDriver):
|
||||
try:
|
||||
volume_info = client.getVolumeByName(src_vref['name'])
|
||||
clone_info = client.cloneVolume(volume['name'], volume_info['id'])
|
||||
return self._update_provider(clone_info)
|
||||
|
||||
model_update = self._update_provider(clone_info)
|
||||
|
||||
# v2 replication check
|
||||
if self._volume_of_replicated_type(volume) and (
|
||||
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}))
|
||||
|
||||
return model_update
|
||||
except Exception as ex:
|
||||
raise exception.VolumeBackendAPIException(ex)
|
||||
finally:
|
||||
@ -654,10 +790,12 @@ class HPELeftHandISCSIDriver(driver.ISCSIDriver):
|
||||
{'value': value, 'key': key})
|
||||
return client_options
|
||||
|
||||
def _update_provider(self, volume_info):
|
||||
def _update_provider(self, volume_info, cluster_vip=None):
|
||||
if not cluster_vip:
|
||||
cluster_vip = self.cluster_vip
|
||||
# TODO(justinsb): Is this always 1? Does it matter?
|
||||
cluster_interface = '1'
|
||||
iscsi_portal = self.cluster_vip + ":3260," + cluster_interface
|
||||
iscsi_portal = cluster_vip + ":3260," + cluster_interface
|
||||
|
||||
return {'provider_location': (
|
||||
"%s %s %s" % (iscsi_portal, volume_info['iscsiIqn'], 0))}
|
||||
@ -1061,3 +1199,459 @@ class HPELeftHandISCSIDriver(driver.ISCSIDriver):
|
||||
def _get_volume_type(self, type_id):
|
||||
ctxt = context.get_admin_context()
|
||||
return volume_types.get_volume_type(ctxt, type_id)
|
||||
|
||||
# v2 replication methods
|
||||
def get_replication_updates(self, context):
|
||||
# TODO(aorourke): the manager does not do anything with these updates.
|
||||
# When that is changed, I will modify this as well.
|
||||
errors = []
|
||||
return errors
|
||||
|
||||
def replication_enable(self, context, volume):
|
||||
"""Enable replication on a replication capable volume."""
|
||||
model_update = {}
|
||||
# If replication is not enabled and the volume is of replicated type,
|
||||
# we treat this as an error.
|
||||
if not self._replication_enabled:
|
||||
msg = _LE("Enabling replication failed because replication is "
|
||||
"not properly configured.")
|
||||
LOG.error(msg)
|
||||
model_update['replication_status'] = "error"
|
||||
else:
|
||||
client = self._login()
|
||||
try:
|
||||
if self._do_volume_replication_setup(volume, client):
|
||||
model_update['replication_status'] = "enabled"
|
||||
else:
|
||||
model_update['replication_status'] = "error"
|
||||
finally:
|
||||
self._logout(client)
|
||||
|
||||
return model_update
|
||||
|
||||
def replication_disable(self, context, volume):
|
||||
"""Disable replication on the specified volume."""
|
||||
model_update = {}
|
||||
# If replication is not enabled and the volume is of replicated type,
|
||||
# we treat this as an error.
|
||||
if self._replication_enabled:
|
||||
model_update['replication_status'] = 'disabled'
|
||||
vol_name = volume['name']
|
||||
|
||||
client = self._login()
|
||||
try:
|
||||
name = vol_name + self.REP_SCHEDULE_SUFFIX + "_Pri"
|
||||
client.stopRemoteSnapshotSchedule(name)
|
||||
except Exception as ex:
|
||||
msg = (_LE("There was a problem disabling replication on "
|
||||
"volume '%(name)s': %(error)s") %
|
||||
{'name': vol_name,
|
||||
'error': six.text_type(ex)})
|
||||
LOG.error(msg)
|
||||
model_update['replication_status'] = 'disable_failed'
|
||||
finally:
|
||||
self._logout(client)
|
||||
else:
|
||||
msg = _LE("Disabling replication failed because replication is "
|
||||
"not properly configured.")
|
||||
LOG.error(msg)
|
||||
model_update['replication_status'] = 'error'
|
||||
|
||||
return model_update
|
||||
|
||||
def replication_failover(self, context, volume, secondary):
|
||||
"""Force failover to a secondary replication target."""
|
||||
failover_target = None
|
||||
for target in self._replication_targets:
|
||||
if target['target_device_id'] == secondary:
|
||||
failover_target = target
|
||||
break
|
||||
|
||||
if not failover_target:
|
||||
msg = _("A valid secondary target MUST be specified in order "
|
||||
"to failover.")
|
||||
LOG.error(msg)
|
||||
raise exception.VolumeBackendAPIException(data=msg)
|
||||
|
||||
# Try and stop the remote snapshot schedule. If the priamry array is
|
||||
# down, we will continue with the failover.
|
||||
client = None
|
||||
try:
|
||||
client = self._login(timeout=30)
|
||||
name = volume['name'] + self.REP_SCHEDULE_SUFFIX + "_Pri"
|
||||
client.stopRemoteSnapshotSchedule(name)
|
||||
except Exception:
|
||||
LOG.warning(_LW("The primary array is currently offline, remote "
|
||||
"copy has been automatically paused."))
|
||||
pass
|
||||
finally:
|
||||
self._logout(client)
|
||||
|
||||
# Update provider location to the new array.
|
||||
cl = None
|
||||
model_update = {}
|
||||
try:
|
||||
cl = self._create_replication_client(failover_target)
|
||||
# Make the volume primary so it can be attached after a fail-over.
|
||||
cl.makeVolumePrimary(volume['name'])
|
||||
# Stop snapshot schedule
|
||||
try:
|
||||
name = volume['name'] + self.REP_SCHEDULE_SUFFIX + "_Rmt"
|
||||
cl.stopRemoteSnapshotSchedule(name)
|
||||
except Exception:
|
||||
pass
|
||||
# Update the provider info for a proper fail-over.
|
||||
volume_info = cl.getVolumeByName(volume['name'])
|
||||
model_update = self._update_provider(
|
||||
volume_info, cluster_vip=failover_target['cluster_vip'])
|
||||
except Exception as ex:
|
||||
msg = (_("The fail-over was unsuccessful: %s") %
|
||||
six.text_type(ex))
|
||||
LOG.error(msg)
|
||||
raise exception.VolumeBackendAPIException(data=msg)
|
||||
finally:
|
||||
self._destroy_replication_client(cl)
|
||||
|
||||
rep_data = json.loads(volume['replication_driver_data'])
|
||||
rep_data['location'] = failover_target['hpelefthand_api_url']
|
||||
replication_driver_data = json.dumps(rep_data)
|
||||
model_update['replication_driver_data'] = replication_driver_data
|
||||
if failover_target['managed_backend_name']:
|
||||
# We want to update the volumes host if our target is managed.
|
||||
model_update['host'] = failover_target['managed_backend_name']
|
||||
|
||||
return model_update
|
||||
|
||||
def list_replication_targets(self, context, volume):
|
||||
"""Provides a means to obtain replication targets for a volume."""
|
||||
client = None
|
||||
try:
|
||||
client = self._login(timeout=30)
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
self._logout(client)
|
||||
|
||||
replication_targets = []
|
||||
for target in self._replication_targets:
|
||||
list_vals = {}
|
||||
list_vals['target_device_id'] = (
|
||||
target.get('target_device_id'))
|
||||
replication_targets.append(list_vals)
|
||||
|
||||
return {'volume_id': volume['id'],
|
||||
'targets': replication_targets}
|
||||
|
||||
def _do_replication_setup(self):
|
||||
default_san_ssh_port = self.configuration.hpelefthand_ssh_port
|
||||
default_ssh_conn_timeout = self.configuration.ssh_conn_timeout
|
||||
default_san_private_key = self.configuration.san_private_key
|
||||
|
||||
replication_targets = []
|
||||
replication_devices = self.configuration.replication_device
|
||||
if replication_devices:
|
||||
# We do not want to fail if we cannot log into the client here
|
||||
# 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['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))
|
||||
remote_array['cluster_id'] = None
|
||||
remote_array['cluster_vip'] = None
|
||||
array_name = remote_array['target_device_id']
|
||||
|
||||
# Make sure we can log into the array, that it has been
|
||||
# correctly configured, and its API version meets the
|
||||
# minimum requirement.
|
||||
cl = None
|
||||
try:
|
||||
cl = self._create_replication_client(remote_array)
|
||||
api_version = cl.getApiVersion()
|
||||
cluster_info = cl.getClusterByName(
|
||||
remote_array['hpelefthand_clustername'])
|
||||
remote_array['cluster_id'] = cluster_info['id']
|
||||
virtual_ips = cluster_info['virtualIPAddresses']
|
||||
remote_array['cluster_vip'] = virtual_ips[0]['ipV4Address']
|
||||
|
||||
if api_version < MIN_API_VERSION:
|
||||
msg = (_LW("The secondary array must have an API "
|
||||
"version of %(min_ver)s or higher. "
|
||||
"Array '%(target)s' is on %(target_ver)s, "
|
||||
"therefore it will not be added as a valid "
|
||||
"replication target.") %
|
||||
{'min_ver': MIN_API_VERSION,
|
||||
'target': array_name,
|
||||
'target_ver': api_version})
|
||||
LOG.warning(msg)
|
||||
elif not self._is_valid_replication_array(remote_array):
|
||||
msg = (_LW("'%s' is not a valid replication array. "
|
||||
"In order to be valid, target_device_id, "
|
||||
"hpelefthand_api_url, "
|
||||
"hpelefthand_username, "
|
||||
"hpelefthand_password, and "
|
||||
"hpelefthand_clustername, "
|
||||
"must be specified. If the target is "
|
||||
"managed, managed_backend_name must be set "
|
||||
"as well.") % array_name)
|
||||
LOG.warning(msg)
|
||||
else:
|
||||
replication_targets.append(remote_array)
|
||||
except Exception:
|
||||
msg = (_LE("Could not log in to LeftHand array (%s) with "
|
||||
"the provided credentials.") % array_name)
|
||||
LOG.error(msg)
|
||||
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
|
||||
|
||||
def _is_replication_configured_correct(self):
|
||||
rep_flag = True
|
||||
# Make sure there is at least one replication target.
|
||||
if len(self._replication_targets) < 1:
|
||||
LOG.error(_LE("There must be at least one valid replication "
|
||||
"device configured."))
|
||||
rep_flag = False
|
||||
return rep_flag
|
||||
|
||||
def _volume_of_replicated_type(self, volume):
|
||||
replicated_type = False
|
||||
volume_type_id = volume.get('volume_type_id')
|
||||
if volume_type_id:
|
||||
volume_type = self._get_volume_type(volume_type_id)
|
||||
|
||||
extra_specs = volume_type.get('extra_specs')
|
||||
if extra_specs and 'replication_enabled' in extra_specs:
|
||||
rep_val = extra_specs['replication_enabled']
|
||||
replicated_type = (rep_val == "<is> True")
|
||||
|
||||
return replicated_type
|
||||
|
||||
def _does_snapshot_schedule_exist(self, schedule_name, client):
|
||||
try:
|
||||
exists = client.doesRemoteSnapshotScheduleExist(schedule_name)
|
||||
except Exception:
|
||||
exists = False
|
||||
return exists
|
||||
|
||||
def _do_volume_replication_setup(self, volume, client, optional=None):
|
||||
"""This function will do or ensure the following:
|
||||
|
||||
-Create volume on main array (already done in create_volume)
|
||||
-Create volume on secondary array
|
||||
-Make volume remote on secondary array
|
||||
-Create the snapshot schedule
|
||||
|
||||
If anything here fails, we will need to clean everything up in
|
||||
reverse order, including the original volume.
|
||||
"""
|
||||
schedule_name = volume['name'] + self.REP_SCHEDULE_SUFFIX
|
||||
# If there is already a snapshot schedule, the volume is setup
|
||||
# for replication on the backend. Start the schedule and return
|
||||
# success.
|
||||
if self._does_snapshot_schedule_exist(schedule_name + "_Pri", client):
|
||||
try:
|
||||
client.startRemoteSnapshotSchedule(schedule_name + "_Pri")
|
||||
except Exception:
|
||||
pass
|
||||
return True
|
||||
|
||||
# Grab the extra_spec entries for replication and make sure they
|
||||
# are set correctly.
|
||||
volume_type = self._get_volume_type(volume["volume_type_id"])
|
||||
extra_specs = volume_type.get("extra_specs")
|
||||
|
||||
# Get and check replication sync period
|
||||
replication_sync_period = extra_specs.get(
|
||||
self.EXTRA_SPEC_REP_SYNC_PERIOD)
|
||||
if replication_sync_period:
|
||||
replication_sync_period = int(replication_sync_period)
|
||||
if replication_sync_period < self.MIN_REP_SYNC_PERIOD:
|
||||
msg = (_("The replication sync period must be at least %s "
|
||||
"seconds.") % self.MIN_REP_SYNC_PERIOD)
|
||||
LOG.error(msg)
|
||||
raise exception.VolumeBackendAPIException(data=msg)
|
||||
else:
|
||||
# If there is no extra_spec value for replication sync period, we
|
||||
# will default it to the required minimum and log a warning.
|
||||
replication_sync_period = self.MIN_REP_SYNC_PERIOD
|
||||
LOG.warning(_LW("There was no extra_spec value for %(spec_name)s, "
|
||||
"so the default value of %(def_val)s will be "
|
||||
"used. To overwrite this, set this value in the "
|
||||
"volume type extra_specs."),
|
||||
{'spec_name': self.EXTRA_SPEC_REP_SYNC_PERIOD,
|
||||
'def_val': self.MIN_REP_SYNC_PERIOD})
|
||||
|
||||
# Get and check retention count
|
||||
retention_count = extra_specs.get(
|
||||
self.EXTRA_SPEC_REP_RETENTION_COUNT)
|
||||
if retention_count:
|
||||
retention_count = int(retention_count)
|
||||
if retention_count > self.MAX_RETENTION_COUNT:
|
||||
msg = (_("The retention count must be %s or less.") %
|
||||
self.MAX_RETENTION_COUNT)
|
||||
LOG.error(msg)
|
||||
raise exception.VolumeBackendAPIException(data=msg)
|
||||
else:
|
||||
# If there is no extra_spec value for retention count, we
|
||||
# will default it and log a warning.
|
||||
retention_count = self.DEFAULT_RETENTION_COUNT
|
||||
LOG.warning(_LW("There was no extra_spec value for %(spec_name)s, "
|
||||
"so the default value of %(def_val)s will be "
|
||||
"used. To overwrite this, set this value in the "
|
||||
"volume type extra_specs."),
|
||||
{'spec_name': self.EXTRA_SPEC_REP_RETENTION_COUNT,
|
||||
'def_val': self.DEFAULT_RETENTION_COUNT})
|
||||
|
||||
# Get and checkout remote retention count
|
||||
remote_retention_count = extra_specs.get(
|
||||
self.EXTRA_SPEC_REP_REMOTE_RETENTION_COUNT)
|
||||
if remote_retention_count:
|
||||
remote_retention_count = int(remote_retention_count)
|
||||
if remote_retention_count > self.MAX_REMOTE_RETENTION_COUNT:
|
||||
msg = (_("The remote retention count must be %s or less.") %
|
||||
self.MAX_REMOTE_RETENTION_COUNT)
|
||||
LOG.error(msg)
|
||||
raise exception.VolumeBackendAPIException(data=msg)
|
||||
else:
|
||||
# If there is no extra_spec value for remote retention count, we
|
||||
# will default it and log a warning.
|
||||
remote_retention_count = self.DEFAULT_REMOTE_RETENTION_COUNT
|
||||
spec_name = self.EXTRA_SPEC_REP_REMOTE_RETENTION_COUNT
|
||||
LOG.warning(_LW("There was no extra_spec value for %(spec_name)s, "
|
||||
"so the default value of %(def_val)s will be "
|
||||
"used. To overwrite this, set this value in the "
|
||||
"volume type extra_specs."),
|
||||
{'spec_name': spec_name,
|
||||
'def_val': self.DEFAULT_REMOTE_RETENTION_COUNT})
|
||||
|
||||
cl = None
|
||||
try:
|
||||
# Create volume on secondary system
|
||||
for remote_target in self._replication_targets:
|
||||
cl = self._create_replication_client(remote_target)
|
||||
|
||||
if optional:
|
||||
optional['clusterName'] = (
|
||||
remote_target['hpelefthand_clustername'])
|
||||
cl.createVolume(volume['name'],
|
||||
remote_target['cluster_id'],
|
||||
volume['size'] * units.Gi,
|
||||
optional)
|
||||
|
||||
# Make secondary volume a remote volume
|
||||
# NOTE: The snapshot created when making a volume remote is
|
||||
# not managed by cinder. This snapshot will be removed when
|
||||
# _do_volume_replication_destroy is called.
|
||||
snap_name = volume['name'] + self.REP_SNAPSHOT_SUFFIX
|
||||
cl.makeVolumeRemote(volume['name'], snap_name)
|
||||
|
||||
# A remote IP address is needed from the cluster in order to
|
||||
# create the snapshot schedule.
|
||||
remote_ip = cl.getIPFromCluster(
|
||||
remote_target['hpelefthand_clustername'])
|
||||
|
||||
# Destroy remote client
|
||||
self._destroy_replication_client(cl)
|
||||
|
||||
# Create remote snapshot schedule on the primary system.
|
||||
# We want to start the remote snapshot schedule instantly; a
|
||||
# date in the past will do that. We will use the Linux epoch
|
||||
# date formatted to ISO 8601 (YYYY-MM-DDTHH:MM:SSZ).
|
||||
start_date = "1970-01-01T00:00:00Z"
|
||||
remote_vol_name = volume['name']
|
||||
|
||||
client.createRemoteSnapshotSchedule(
|
||||
volume['name'],
|
||||
schedule_name,
|
||||
replication_sync_period,
|
||||
start_date,
|
||||
retention_count,
|
||||
remote_target['hpelefthand_clustername'],
|
||||
remote_retention_count,
|
||||
remote_vol_name,
|
||||
remote_ip,
|
||||
remote_target['hpelefthand_username'],
|
||||
remote_target['hpelefthand_password'])
|
||||
|
||||
return True
|
||||
except Exception as ex:
|
||||
# Destroy the replication client that was created
|
||||
self._destroy_replication_client(cl)
|
||||
# Deconstruct what we tried to create
|
||||
self._do_volume_replication_destroy(volume, client)
|
||||
msg = (_("There was an error setting up a remote schedule "
|
||||
"on the LeftHand arrays: ('%s'). The volume will not be "
|
||||
"recognized as replication type.") %
|
||||
six.text_type(ex))
|
||||
LOG.error(msg)
|
||||
raise exception.VolumeBackendAPIException(data=msg)
|
||||
|
||||
def _do_volume_replication_destroy(self, volume, client):
|
||||
"""This will remove all dependencies of a replicated volume
|
||||
|
||||
It should be used when deleting a replication enabled volume
|
||||
or if setting up a remote copy group fails. It will try and do the
|
||||
following:
|
||||
-Delete the snapshot schedule
|
||||
-Delete volume and snapshots on secondary array
|
||||
-Delete volume and snapshots on primary array
|
||||
"""
|
||||
# Delete snapshot schedule
|
||||
try:
|
||||
schedule_name = volume['name'] + self.REP_SCHEDULE_SUFFIX
|
||||
client.deleteRemoteSnapshotSchedule(schedule_name)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Delete volume on secondary array(s)
|
||||
remote_vol_name = volume['name']
|
||||
for remote_target in self._replication_targets:
|
||||
try:
|
||||
cl = self._create_replication_client(remote_target)
|
||||
volume_info = cl.getVolumeByName(remote_vol_name)
|
||||
cl.deleteVolume(volume_info['id'])
|
||||
except Exception:
|
||||
pass
|
||||
finally:
|
||||
# Destroy the replication client that was created
|
||||
self._destroy_replication_client(cl)
|
||||
|
||||
# Delete volume on primary array
|
||||
try:
|
||||
volume_info = client.getVolumeByName(volume['name'])
|
||||
client.deleteVolume(volume_info['id'])
|
||||
except Exception:
|
||||
pass
|
||||
|
@ -0,0 +1,3 @@
|
||||
---
|
||||
features:
|
||||
- Added managed v2 replication support to the HPE LeftHand driver.
|
Loading…
x
Reference in New Issue
Block a user