Storwize: Implement v2 replication

Storwize supports three major types for volume replications:
split IO, global mirror and metro mirror.

This patch is dedicated to implement the replication for
the modes of global mirror and metro mirror. Mirror
establishes a Global/Metro Mirror relationship between
two volumes of equal size. The volumes in a Mirror
relationship are referred to as the primary volume and
the replica volume. The replication_mode in
replication_device must be set to global or metro.

The volume type needs to associate with the extra spec
with 'replication_enabled' equaling to "<is> True", and
'replication_type' equaling to '<in> global' or '<in>
metro'.

What is supported with replication:
* create volume
* create volume from snapshot
* clone a volume
When a volume is created and replication is enabled, the
replica volume is also created on the back-end enabled
for replicas.

What is not supported with replication yet:
* volume migration
* volume retype
The replica volume will not be created or moved after migration
or retype of a replicated volume. Admins should be aware,
when migrating or retyping a volume to a type with replication
enabled, that the replica will not be automatically created.

The replication can be configured via either multi-backend
on one cinder volume node, or on separate cinder volume
nodes.

Options to be put in cinder.conf, where the primary back-end
is located:

enabled_backends = sv1, sv2 (if enabling multi-backends)

[sv1]
san_login = admin
san_password = admin
san_ip = 192.168.0.11
volume_driver = cinder.volume.drivers.ibm.storwize_svc.\
                StorwizeSVCDriver
volume_backend_name = sv1
storwize_svc_volpool_name=cinder
replication_device = managed_backend_name:second_host@sv2#sv2,
                     replication_mode:global,
                     target_device_id:svc_id_target,
                     san_ip:192.168.0.12,san_login:admin,
                     san_password:admin,pool_name:cinder_target

Options to be put in cinder.conf, where the secondary
back-end is connected:

[sv2]
san_login = admin
san_password = admin
san_ip = 192.168.0.12
volume_driver = cinder.volume.drivers.ibm.storwize_svc.\
                StorwizeSVCDriver
volume_backend_name = sv2
storwize_svc_volpool_name=cinder_target

Partial-implements: blueprint ibm-storwize-v2-replication
DocImpact

Change-Id: I2ad5be69b2814d3b974c963828585fa15446d772
This commit is contained in:
Vincent Hou 2016-02-02 14:20:02 -05:00
parent f2f241a440
commit 05f8a52301
4 changed files with 960 additions and 11 deletions

View File

@ -21,6 +21,7 @@ Tests for the IBM Storwize family and SVC volume driver.
import random import random
import re import re
import time import time
import uuid
import mock import mock
from oslo_concurrency import processutils from oslo_concurrency import processutils
@ -36,6 +37,8 @@ from cinder import test
from cinder.tests.unit import utils as testutils from cinder.tests.unit import utils as testutils
from cinder import utils from cinder import utils
from cinder.volume import configuration as conf from cinder.volume import configuration as conf
from cinder.volume.drivers.ibm.storwize_svc import (
replication as storwize_rep)
from cinder.volume.drivers.ibm.storwize_svc import storwize_svc_common from cinder.volume.drivers.ibm.storwize_svc import storwize_svc_common
from cinder.volume.drivers.ibm.storwize_svc import storwize_svc_fc from cinder.volume.drivers.ibm.storwize_svc import storwize_svc_fc
from cinder.volume.drivers.ibm.storwize_svc import storwize_svc_iscsi from cinder.volume.drivers.ibm.storwize_svc import storwize_svc_iscsi
@ -3173,6 +3176,57 @@ class StorwizeSVCCommonDriverTestCase(test.TestCase):
self.driver.delete_snapshot(snap) self.driver.delete_snapshot(snap)
self.driver.delete_volume(volume) self.driver.delete_volume(volume)
@mock.patch.object(storwize_rep.StorwizeSVCReplicationGlobalMirror,
'create_relationship')
@mock.patch.object(storwize_rep.StorwizeSVCReplicationGlobalMirror,
'extend_target_volume')
@mock.patch.object(storwize_svc_common.StorwizeHelpers,
'delete_relationship')
@mock.patch.object(storwize_svc_common.StorwizeHelpers,
'get_relationship_info')
def test_storwize_svc_extend_volume_replication(self,
get_relationship,
delete_relationship,
extend_target_volume,
create_relationship):
fake_target = mock.Mock()
rep_type = 'global'
self.driver.replications[rep_type] = (
self.driver.replication_factory(rep_type, fake_target))
volume = self._create_volume()
volume['replication_status'] = 'enabled'
fake_target_vol = 'vol-target-id'
get_relationship.return_value = {'aux_vdisk_name': fake_target_vol}
with mock.patch.object(
self.driver,
'_get_volume_replicated_type_mirror') as mirror_type:
mirror_type.return_value = 'global'
self.driver.extend_volume(volume, '13')
attrs = self.driver._helpers.get_vdisk_attributes(volume['name'])
vol_size = int(attrs['capacity']) / units.Gi
self.assertAlmostEqual(vol_size, 13)
delete_relationship.assert_called_once_with(volume)
extend_target_volume.assert_called_once_with(fake_target_vol,
12)
create_relationship.assert_called_once_with(volume,
fake_target_vol)
self.driver.delete_volume(volume)
def test_storwize_svc_extend_volume_replication_failover(self):
volume = self._create_volume()
volume['replication_status'] = 'failed-over'
with mock.patch.object(
self.driver,
'_get_volume_replicated_type_mirror') as mirror_type:
mirror_type.return_value = 'global'
self.driver.extend_volume(volume, '13')
attrs = self.driver._helpers.get_vdisk_attributes(volume['name'])
vol_size = int(attrs['capacity']) / units.Gi
self.assertAlmostEqual(vol_size, 13)
self.driver.delete_volume(volume)
def _check_loc_info(self, capabilities, expected): def _check_loc_info(self, capabilities, expected):
host = {'host': 'foo', 'capabilities': capabilities} host = {'host': 'foo', 'capabilities': capabilities}
vol = {'name': 'test', 'id': 1, 'size': 1} vol = {'name': 'test', 'id': 1, 'size': 1}
@ -4315,3 +4369,277 @@ class StorwizeSSHTestCase(test.TestCase):
self.assertRaises(exception.VolumeBackendAPIException, self.assertRaises(exception.VolumeBackendAPIException,
self.storwize_ssh.mkvdiskhostmap, self.storwize_ssh.mkvdiskhostmap,
'HOST3', 9999, 511, True) 'HOST3', 9999, 511, True)
class StorwizeSVCReplicationMirrorTestCase(test.TestCase):
rep_type = 'global'
mirror_class = storwize_rep.StorwizeSVCReplicationGlobalMirror
def setUp(self):
super(StorwizeSVCReplicationMirrorTestCase, self).setUp()
self.svc_driver = storwize_svc_iscsi.StorwizeSVCISCSIDriver(
configuration=conf.Configuration(None))
extra_spec_rep_type = '<in> ' + self.rep_type
fake_target = {"managed_backend_name": "second_host@sv2#sv2",
"replication_mode": self.rep_type,
"target_device_id": "svc_id_target",
"san_ip": "192.168.10.23",
"san_login": "admin",
"san_password": "admin",
"pool_name": "cinder_target"}
self.fake_targets = [fake_target]
self.driver = self.mirror_class(self.svc_driver, fake_target,
storwize_svc_common.StorwizeHelpers)
self.svc_driver.configuration.set_override('replication_device',
self.fake_targets)
self.svc_driver._replication_targets = self.fake_targets
self.svc_driver._replication_enabled = True
self.svc_driver.replications[self.rep_type] = (
self.svc_driver.replication_factory(self.rep_type, fake_target))
self.ctxt = context.get_admin_context()
rand_id = six.text_type(uuid.uuid4())
self.volume = {'name': 'volume-%s' % rand_id,
'size': 10, 'id': '%s' % rand_id,
'volume_type_id': None,
'mdisk_grp_name': 'openstack',
'replication_status': 'disabled',
'replication_extended_status': None,
'volume_metadata': None}
spec = {'replication_enabled': '<is> True',
'replication_type': extra_spec_rep_type}
type_ref = volume_types.create(self.ctxt, "replication", spec)
self.replication_type = volume_types.get_volume_type(self.ctxt,
type_ref['id'])
self.volume['volume_type_id'] = self.replication_type['id']
self.volume['volume_type'] = self.replication_type
def test_storwize_do_replication_setup(self):
self.svc_driver.configuration.set_override('san_ip', "192.168.10.23")
self.svc_driver.configuration.set_override('replication_device',
self.fake_targets)
self.svc_driver._do_replication_setup()
def test_storwize_do_replication_setup_unmanaged(self):
fake_target = {"replication_mode": self.rep_type,
"target_device_id": "svc_id_target",
"san_ip": "192.168.10.23",
"san_login": "admin",
"san_password": "admin",
"pool_name": "cinder_target"}
fake_targets = [fake_target]
self.svc_driver.configuration.set_override('san_ip', "192.168.10.23")
self.svc_driver.configuration.set_override('replication_device',
fake_targets)
self.assertRaises(exception.InvalidConfigurationValue,
self.svc_driver._do_replication_setup)
@mock.patch.object(storwize_svc_common.StorwizeHelpers, 'create_vdisk')
@mock.patch.object(storwize_svc_common.StorwizeHelpers, 'get_vdisk_params')
@mock.patch.object(context, 'get_admin_context')
@mock.patch.object(mirror_class, 'volume_replication_setup')
def test_storwize_create_volume_with_mirror_replication(self,
rep_setup,
ctx,
get_vdisk_params,
create_vdisk):
ctx.return_value = self.ctxt
get_vdisk_params.return_value = {'replication': None,
'qos': None}
self.svc_driver.create_volume(self.volume)
rep_setup.assert_called_once_with(self.ctxt, self.volume)
@mock.patch.object(storwize_svc_common.StorwizeHelpers, 'create_copy')
@mock.patch.object(storwize_svc_common.StorwizeHelpers, 'get_vdisk_params')
@mock.patch.object(context, 'get_admin_context')
@mock.patch.object(mirror_class, 'volume_replication_setup')
def test_storwize_create_volume_from_snap_with_mirror_replication(
self, rep_setup, ctx, get_vdisk_params, create_copy):
ctx.return_value = self.ctxt
get_vdisk_params.return_value = {'replication': None,
'qos': None}
snapshot = {'id': 'snapshot-id',
'name': 'snapshot-name',
'volume_size': 10}
model_update = self.svc_driver.create_volume_from_snapshot(
self.volume, snapshot)
rep_setup.assert_called_once_with(self.ctxt, self.volume)
self.assertEqual({'replication_status': 'enabled'}, model_update)
@mock.patch.object(storwize_svc_common.StorwizeHelpers, 'create_copy')
@mock.patch.object(storwize_svc_common.StorwizeHelpers, 'get_vdisk_params')
@mock.patch.object(context, 'get_admin_context')
@mock.patch.object(mirror_class, 'volume_replication_setup')
def test_storwize_clone_volume_with_mirror_replication(
self, rep_setup, ctx, get_vdisk_params, create_copy):
ctx.return_value = self.ctxt
get_vdisk_params.return_value = {'replication': None,
'qos': None}
rand_id = six.text_type(random.randint(10000, 99999))
target_volume = {'name': 'test_volume%s' % rand_id,
'size': 10, 'id': '%s' % rand_id,
'volume_type_id': None,
'mdisk_grp_name': 'openstack',
'replication_status': 'disabled',
'replication_extended_status': None,
'volume_metadata': None}
target_volume['volume_type_id'] = self.replication_type['id']
target_volume['volume_type'] = self.replication_type
model_update = self.svc_driver.create_cloned_volume(
target_volume, self.volume)
rep_setup.assert_called_once_with(self.ctxt, target_volume)
self.assertEqual({'replication_status': 'enabled'}, model_update)
@mock.patch.object(mirror_class, 'replication_enable')
@mock.patch.object(mirror_class, 'volume_replication_setup')
def test_storwize_replication_enable(self, rep_setup,
replication_enable):
self.svc_driver.replication_enable(self.ctxt, self.volume)
replication_enable.assert_called_once_with(self.ctxt, self.volume)
@mock.patch.object(mirror_class,
'replication_disable')
@mock.patch.object(mirror_class,
'volume_replication_setup')
def test_storwize_replication_disable(self, rep_setup,
replication_disable):
self.svc_driver.replication_disable(self.ctxt, self.volume)
replication_disable.assert_called_once_with(self.ctxt, self.volume)
@mock.patch.object(mirror_class,
'replication_failover')
@mock.patch.object(mirror_class,
'volume_replication_setup')
def test_storwize_replication_failover(self, rep_setup,
replication_failover):
fake_secondary = 'svc_id_target'
self.svc_driver.replication_failover(self.ctxt, self.volume,
fake_secondary)
replication_failover.assert_called_once_with(self.ctxt, self.volume,
fake_secondary)
@mock.patch.object(mirror_class,
'list_replication_targets')
def test_storwize_list_replication_targets(self, list_targets):
fake_targets = [{"managed_backend_name": "second_host@sv2#sv2",
"type": "managed",
"target_device_id": "svc_id_target",
"pool_name": "cinder_target"}]
list_targets.return_value = fake_targets
expected_resp = {'targets': fake_targets,
'volume_id': self.volume['id']}
targets = self.svc_driver.list_replication_targets(self.ctxt,
self.volume)
list_targets.assert_called_once_with(self.ctxt, self.volume)
self.assertEqual(expected_resp, targets)
@mock.patch.object(mirror_class,
'_partnership_validate_create')
@mock.patch.object(storwize_svc_common.StorwizeHelpers,
'get_system_info')
def test_establish_target_partnership(self, get_system_info,
partnership_validate_create):
source_system_name = 'source_vol'
target_system_name = 'target_vol'
self.svc_driver.configuration.set_override('san_ip',
"192.168.10.21")
get_system_info.side_effect = [{'system_name': source_system_name},
{'system_name': target_system_name}]
self.driver.establish_target_partnership()
expected_calls = [mock.call(self.svc_driver._helpers,
'target_vol', '192.168.10.23'),
mock.call(self.driver.target_helpers,
'source_vol', '192.168.10.21')]
partnership_validate_create.assert_has_calls(expected_calls)
@mock.patch.object(storwize_svc_common.StorwizeHelpers,
'create_relationship')
@mock.patch.object(storwize_svc_common.StorwizeHelpers,
'get_system_info')
@mock.patch.object(storwize_svc_common.StorwizeHelpers,
'create_vdisk')
@mock.patch.object(storwize_svc_common.StorwizeHelpers,
'get_vdisk_params')
@mock.patch.object(storwize_svc_common.StorwizeHelpers,
'get_vdisk_attributes')
@mock.patch.object(storwize_svc_common.StorwizeHelpers,
'get_relationship_info')
def test_replication_enable(self, get_relationship_info,
get_vdisk_attributes,
get_vdisk_params,
create_vdisk,
get_system_info,
create_relationship):
fake_system = 'fake_system'
fake_params = mock.Mock()
get_relationship_info.return_value = None
get_vdisk_attributes.return_value = None
get_vdisk_params.return_value = fake_params
get_system_info.return_value = {'system_name': fake_system}
model_update = self.driver.replication_enable(self.ctxt,
self.volume)
get_relationship_info.assert_called_once_with(self.volume)
get_vdisk_attributes.assert_called_once_with(self.volume['name'])
create_vdisk.assert_called_once_with(self.volume['name'],
'10', 'gb', 'cinder_target',
fake_params)
create_relationship.assert_called_once_with(self.volume['name'],
self.volume['name'],
fake_system,
self.driver.asyncmirror)
self.assertEqual({'replication_status': 'enabled'}, model_update)
@mock.patch.object(storwize_svc_common.StorwizeHelpers,
'delete_vdisk')
@mock.patch.object(storwize_svc_common.StorwizeHelpers,
'delete_relationship')
@mock.patch.object(storwize_svc_common.StorwizeHelpers,
'get_relationship_info')
def test_replication_disable(self, get_relationship_info,
delete_relationship,
delete_vdisk):
fake_target_vol_name = 'fake_target_vol_name'
get_relationship_info.return_value = {'aux_vdisk_name':
fake_target_vol_name}
model_update = self.driver.replication_disable(self.ctxt,
self.volume)
delete_relationship.assert_called_once_with(self.volume['name'])
delete_vdisk.assert_called_once_with(fake_target_vol_name,
False)
self.assertEqual({'replication_status': 'disabled'}, model_update)
@mock.patch.object(storwize_svc_common.StorwizeHelpers,
'delete_relationship')
@mock.patch.object(storwize_svc_common.StorwizeHelpers,
'get_relationship_info')
def test_replication_failover(self, get_relationship_info,
delete_relationship):
secondary = 'svc_id_target'
fake_id = '546582b2-bafb-43cc-b765-bd738ab148c8'
expected_model_update = {'host': 'second_host@sv2#sv2',
'_name_id': fake_id}
fake_name = 'volume-' + fake_id
get_relationship_info.return_value = {'aux_vdisk_name':
fake_name}
model_update = self.driver.replication_failover(self.ctxt,
self.volume,
secondary)
delete_relationship.assert_called_once_with(self.volume['name'])
self.assertEqual(expected_model_update, model_update)
def test_list_replication_targets(self):
fake_targets = [{'target_device_id': 'svc_id_target'}]
targets = self.driver.list_replication_targets(self.ctxt,
self.volume)
self.assertEqual(fake_targets, targets)
class StorwizeSVCReplicationMetroMirrorTestCase(
StorwizeSVCReplicationMirrorTestCase):
rep_type = 'metro'
mirror_class = storwize_rep.StorwizeSVCReplicationMetroMirror
def setUp(self):
super(StorwizeSVCReplicationMetroMirrorTestCase, self).setUp()

View File

@ -14,10 +14,19 @@
# under the License. # under the License.
# #
import random
import uuid
from eventlet import greenthread
from oslo_concurrency import processutils
from oslo_log import log as logging from oslo_log import log as logging
from oslo_utils import excutils
import six
from cinder import exception from cinder import exception
from cinder.i18n import _, _LI from cinder.i18n import _, _LE, _LI
from cinder import ssh_utils
from cinder import utils
from cinder.volume import volume_types from cinder.volume import volume_types
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
@ -63,10 +72,16 @@ class StorwizeSVCReplication(object):
class StorwizeSVCReplicationStretchedCluster(StorwizeSVCReplication): class StorwizeSVCReplicationStretchedCluster(StorwizeSVCReplication):
"""Support for Storwize/SVC stretched cluster mode replication.""" """Support for Storwize/SVC stretched cluster mode replication.
def __init__(self, driver): This stretched cluster mode implements volume replication in terms of
adding a copy to an existing volume, which changes a nonmirrored volume
into a mirrored volume.
"""
def __init__(self, driver, replication_target=None):
super(StorwizeSVCReplicationStretchedCluster, self).__init__(driver) super(StorwizeSVCReplicationStretchedCluster, self).__init__(driver)
self.target = replication_target or {}
def create_replica(self, ctxt, volume, vol_type = None): def create_replica(self, ctxt, volume, vol_type = None):
# if vol_type is None, use the source volume type # if vol_type is None, use the source volume type
@ -193,3 +208,240 @@ class StorwizeSVCReplicationStretchedCluster(StorwizeSVCReplication):
data = {} data = {}
data['replication'] = True data['replication'] = True
return data return data
class StorwizeSVCReplicationGlobalMirror(
StorwizeSVCReplicationStretchedCluster):
"""Support for Storwize/SVC global mirror mode replication.
Global Mirror establishes a Global Mirror relationship between
two volumes of equal size. The volumes in a Global Mirror relationship
are referred to as the master (source) volume and the auxiliary
(target) volume. This mode is dedicated to the asynchronous volume
replication.
"""
asyncmirror = True
UUID_LEN = 36
def __init__(self, driver, replication_target=None, target_helpers=None):
super(StorwizeSVCReplicationGlobalMirror, self).__init__(
driver, replication_target)
self.sshpool = None
self.target_helpers = target_helpers(self._run_ssh)
def _partnership_validate_create(self, client, remote_name, remote_ip):
try:
partnership_info = client.get_partnership_info(
remote_name)
if not partnership_info:
candidate_info = client.get_partnershipcandidate_info(
remote_name)
if not candidate_info:
client.mkippartnership(remote_ip)
else:
client.mkfcpartnership(remote_name)
elif partnership_info['partnership'] == (
'fully_configured_stopped'):
client.startpartnership(partnership_info['id'])
except Exception:
msg = (_('Unable to establish the partnership with '
'the Storwize cluster %s.'), remote_name)
LOG.error(msg)
raise exception.VolumeDriverException(message=msg)
def establish_target_partnership(self):
local_system_info = self.driver._helpers.get_system_info()
target_system_info = self.target_helpers.get_system_info()
local_system_name = local_system_info['system_name']
target_system_name = target_system_info['system_name']
local_ip = self.driver.configuration.safe_get('san_ip')
target_ip = self.target.get('san_ip')
self._partnership_validate_create(self.driver._helpers,
target_system_name, target_ip)
self._partnership_validate_create(self.target_helpers,
local_system_name, local_ip)
def _run_ssh(self, cmd_list, check_exit_code=True, attempts=1):
utils.check_ssh_injection(cmd_list)
# TODO(vhou): We'll have a common method in ssh_utils to take
# care of this _run_ssh method.
command = ' '. join(cmd_list)
if not self.sshpool:
self.sshpool = ssh_utils.SSHPool(
self.target.get('san_ip'),
self.target.get('san_ssh_port', 22),
self.target.get('ssh_conn_timeout', 30),
self.target.get('san_login'),
password=self.target.get('san_password'),
privatekey=self.target.get('san_private_key', ''),
min_size=self.target.get('ssh_min_pool_conn', 1),
max_size=self.target.get('ssh_max_pool_conn', 5),)
last_exception = None
try:
with self.sshpool.item() as ssh:
while attempts > 0:
attempts -= 1
try:
return processutils.ssh_execute(
ssh, command, check_exit_code=check_exit_code)
except Exception as e:
LOG.error(six.text_type(e))
last_exception = e
greenthread.sleep(random.randint(20, 500) / 100.0)
try:
raise processutils.ProcessExecutionError(
exit_code=last_exception.exit_code,
stdout=last_exception.stdout,
stderr=last_exception.stderr,
cmd=last_exception.cmd)
except AttributeError:
raise processutils.ProcessExecutionError(
exit_code=-1, stdout="",
stderr="Error running SSH command",
cmd=command)
except Exception:
with excutils.save_and_reraise_exception():
LOG.error(_LE("Error running SSH command: %s"), command)
def volume_replication_setup(self, context, vref):
target_vol_name = vref['name']
try:
attr = self.target_helpers.get_vdisk_attributes(target_vol_name)
if attr:
# If the volume name exists in the target pool, we need
# to change to a different target name.
vol_id = six.text_type(uuid.uuid4())
prefix = vref['name'][0:len(vref['name']) - len(vol_id)]
target_vol_name = prefix + vol_id
opts = self.driver._get_vdisk_params(vref['volume_type_id'])
pool = self.target.get('pool_name')
self.target_helpers.create_vdisk(target_vol_name,
six.text_type(vref['size']),
'gb', pool, opts)
system_info = self.target_helpers.get_system_info()
self.driver._helpers.create_relationship(
vref['name'], target_vol_name, system_info.get('system_name'),
self.asyncmirror)
except Exception as e:
msg = (_("Unable to set up mirror mode replication for %(vol)s. "
"Exception: %(err)s."), {'vol': vref['id'],
'err': e})
LOG.error(msg)
raise exception.VolumeDriverException(message=msg)
def create_relationship(self, vref, target_vol_name):
if not target_vol_name:
return
try:
system_info = self.target_helpers.get_system_info()
self.driver._helpers.create_relationship(
vref['name'], target_vol_name, system_info.get('system_name'),
self.asyncmirror)
except Exception:
msg = (_("Unable to create the relationship for %s."),
vref['name'])
LOG.error(msg)
raise exception.VolumeDriverException(message=msg)
def extend_target_volume(self, target_vol_name, amount):
if not target_vol_name:
return
self.target_helpers.extend_vdisk(target_vol_name, amount)
def delete_target_volume(self, vref):
try:
rel_info = self.driver._helpers.get_relationship_info(vref)
except Exception as e:
msg = (_('Fail to get remote copy information for %(volume)s '
'due to %(err)s.'), {'volume': vref['id'], 'err': e})
LOG.error(msg)
raise exception.VolumeDriverException(data=msg)
if rel_info and rel_info.get('aux_vdisk_name', None):
try:
self.driver._helpers.delete_relationship(vref['name'])
self.driver._helpers.delete_vdisk(
rel_info['aux_vdisk_name'], False)
except Exception as e:
msg = (_('Unable to delete the target volume for '
'volume %(vol)s. Exception: %(err)s.'),
{'vol': vref['id'], 'err': e})
LOG.error(msg)
raise exception.VolumeDriverException(message=msg)
# #### Implementing V2 replication methods #### #
def replication_enable(self, context, vref):
try:
rel_info = self.driver._helpers.get_relationship_info(vref)
except Exception as e:
msg = (_('Fail to get remote copy information for %(volume)s '
'due to %(err)s'), {'volume': vref['id'], 'err': e})
LOG.error(msg)
raise exception.VolumeDriverException(message=msg)
if not rel_info or not rel_info.get('aux_vdisk_name', None):
self.volume_replication_setup(context, vref)
model_update = {'replication_status': 'enabled'}
return model_update
def replication_disable(self, context, vref):
self.delete_target_volume(vref)
model_update = {'replication_status': 'disabled'}
return model_update
def replication_failover(self, context, vref, secondary):
if not self.target or self.target.get('target_device_id') != secondary:
msg = _LE("A valid secondary target MUST be specified in order "
"to failover.")
LOG.error(msg)
# If the admin does not provide a valid secondary, the failover
# will fail, but it is not severe enough to throw an exception.
# The admin can still issue another failover request. That is
# why we tentatively put return None instead of raising an
# exception.
return None
try:
rel_info = self.driver._helpers.get_relationship_info(vref)
target_vol_name = rel_info.get('aux_vdisk_name')
target_vol_id = target_vol_name[-self.UUID_LEN:]
if rel_info:
self.driver._helpers.delete_relationship(vref['name'])
if target_vol_id == vref['id']:
target_vol_id = None
except Exception:
msg = (_('Unable to failover the replication for volume %s.'),
vref['id'])
LOG.error(msg)
raise exception.VolumeDriverException(message=msg)
model_update = {'host': self.target.get('managed_backend_name'),
'_name_id': target_vol_id}
return model_update
def list_replication_targets(self, context, vref):
# For the mode of global mirror, there is only one replication target.
return [{'target_device_id': self.target.get('target_device_id')}]
class StorwizeSVCReplicationMetroMirror(
StorwizeSVCReplicationGlobalMirror):
"""Support for Storwize/SVC metro mirror mode replication.
Metro Mirror establishes a Metro Mirror relationship between
two volumes of equal size. The volumes in a Metro Mirror relationship
are referred to as the master (source) volume and the auxiliary
(target) volume.
"""
asyncmirror = False
def __init__(self, driver, replication_target=None, target_helpers=None):
super(StorwizeSVCReplicationMetroMirror, self).__init__(
driver, replication_target, target_helpers)

View File

@ -17,6 +17,7 @@
import math import math
import random import random
import re import re
import string
import time import time
import unicodedata import unicodedata
@ -263,6 +264,62 @@ class StorwizeSSH(object):
with excutils.save_and_reraise_exception(): with excutils.save_and_reraise_exception():
LOG.error(_LE('Error mapping VDisk-to-host')) LOG.error(_LE('Error mapping VDisk-to-host'))
def mkrcrelationship(self, master, aux, system, name, asyncmirror):
ssh_cmd = ['svctask', 'mkrcrelationship', '-master', master,
'-aux', aux, '-cluster', system, '-name', name]
if asyncmirror:
ssh_cmd.append('-global')
return self.run_ssh_check_created(ssh_cmd)
def rmrcrelationship(self, relationship):
ssh_cmd = ['svctask', 'rmrcrelationship', relationship]
self.run_ssh_assert_no_output(ssh_cmd)
def startrcrelationship(self, rc_rel, primary=None):
ssh_cmd = ['svctask', 'startrcrelationship', '-force']
if primary:
ssh_cmd.extend(['-primary', primary])
ssh_cmd.append(rc_rel)
self.run_ssh_assert_no_output(ssh_cmd)
def stoprcrelationship(self, relationship, access=False):
ssh_cmd = ['svctask', 'stoprcrelationship']
if access:
ssh_cmd.append('-access')
ssh_cmd.append(relationship)
self.run_ssh_assert_no_output(ssh_cmd)
def lsrcrelationship(self, volume_name):
key_value = 'name=%s' % volume_name
ssh_cmd = ['svcinfo', 'lsrcrelationship', '-filtervalue',
key_value, '-delim', '!']
return self.run_ssh_info(ssh_cmd, with_header=True)
def lspartnership(self, system_name):
key_value = 'name=%s' % system_name
ssh_cmd = ['svcinfo', 'lspartnership', '-filtervalue',
key_value, '-delim', '!']
return self.run_ssh_info(ssh_cmd, with_header=True)
def lspartnershipcandidate(self):
ssh_cmd = ['svcinfo', 'lspartnershipcandidate', '-delim', '!']
return self.run_ssh_info(ssh_cmd, with_header=True)
def mkippartnership(self, ip_v4, bandwith):
ssh_cmd = ['svctask', 'mkippartnership', '-type', 'ipv4',
'-clusterip', ip_v4, '-linkbandwidthmbits',
six.text_type(bandwith)]
return self.run_ssh_assert_no_output(ssh_cmd)
def mkfcpartnership(self, system_name, bandwith):
ssh_cmd = ['svctask', 'mkfcpartnership', '-linkbandwidthmbits',
six.text_type(bandwith), system_name]
return self.run_ssh_assert_no_output(ssh_cmd)
def startpartnership(self, partnership_id):
ssh_cmd = ['svctask', 'chpartnership', '-start', partnership_id]
return self.run_ssh_assert_no_output(ssh_cmd)
def rmvdiskhostmap(self, host, vdisk): def rmvdiskhostmap(self, host, vdisk):
ssh_cmd = ['svctask', 'rmvdiskhostmap', '-host', '"%s"' % host, vdisk] ssh_cmd = ['svctask', 'rmvdiskhostmap', '-host', '"%s"' % host, vdisk]
self.run_ssh_assert_no_output(ssh_cmd) self.run_ssh_assert_no_output(ssh_cmd)
@ -1392,6 +1449,69 @@ class StorwizeHelpers(object):
timer.stop() timer.stop()
return ret return ret
def start_relationship(self, volume_name, primary=None):
vol_attrs = self.get_vdisk_attributes(volume_name)
if vol_attrs['RC_name']:
self.ssh.startrcrelationship(vol_attrs['RC_name'], primary)
def stop_relationship(self, volume_name):
vol_attrs = self.get_vdisk_attributes(volume_name)
if vol_attrs['RC_name']:
self.ssh.stoprcrelationship(vol_attrs['RC_name'], access=True)
def create_relationship(self, master, aux, system, asyncmirror):
name = 'rcrel' + ''.join(random.sample(string.digits, 10))
try:
rc_id = self.ssh.mkrcrelationship(master, aux, system, name,
asyncmirror)
except exception.VolumeBackendAPIException as e:
# CMMVC5959E is the code in Stowize storage, meaning that
# there is a relationship that already has this name on the
# master cluster.
if 'CMMVC5959E' not in e:
# If there is no relation between the primary and the
# secondary back-end storage, the exception is raised.
raise
if rc_id:
self.start_relationship(master)
def delete_relationship(self, volume_name):
vol_attrs = self.get_vdisk_attributes(volume_name)
if vol_attrs['RC_name']:
self.ssh.stoprcrelationship(vol_attrs['RC_name'])
self.ssh.rmrcrelationship(vol_attrs['RC_name'])
vol_attrs = self.get_vdisk_attributes(volume_name)
def get_relationship_info(self, volume):
vol_attrs = self.get_vdisk_attributes(volume['name'])
if not vol_attrs or not vol_attrs['RC_name']:
LOG.info(_LI("Unable to get remote copy information for "
"volume %s"), volume['name'])
return
relationship = self.ssh.lsrcrelationship(vol_attrs['RC_name'])
return relationship[0] if len(relationship) > 0 else None
def get_partnership_info(self, system_name):
partnership = self.ssh.lspartnership(system_name)
return partnership[0] if len(partnership) > 0 else None
def get_partnershipcandidate_info(self, system_name):
candidates = self.ssh.lspartnershipcandidate()
for candidate in candidates:
if system_name == candidate['name']:
return candidate
return None
def mkippartnership(self, ip_v4, bandwith=1000):
self.ssh.mkippartnership(ip_v4, bandwith)
def mkfcpartnership(self, system_name, bandwith=1000):
self.ssh.mkfcpartnership(system_name, bandwith)
def startpartnership(self, partnership_id):
self.ssh.startpartnership(partnership_id)
def delete_vdisk(self, vdisk, force): def delete_vdisk(self, vdisk, force):
"""Ensures that vdisk is not part of FC mapping and deletes it.""" """Ensures that vdisk is not part of FC mapping and deletes it."""
LOG.debug('Enter: delete_vdisk: vdisk %s.', vdisk) LOG.debug('Enter: delete_vdisk: vdisk %s.', vdisk)
@ -1703,11 +1823,17 @@ class StorwizeSVCCommonDriver(san.SanDriver,
1.3.3 - Update driver to use ABC metaclasses 1.3.3 - Update driver to use ABC metaclasses
2.0 - Code refactor, split init file and placed shared methods for 2.0 - Code refactor, split init file and placed shared methods for
FC and iSCSI within the StorwizeSVCCommonDriver class FC and iSCSI within the StorwizeSVCCommonDriver class
2.1 - Added replication V2 support to the global/metro mirror
mode
""" """
VERSION = "2.0" VERSION = "2.1"
VDISKCOPYOPS_INTERVAL = 600 VDISKCOPYOPS_INTERVAL = 600
GLOBAL = 'global'
METRO = 'metro'
VALID_REP_TYPES = (GLOBAL, METRO)
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(StorwizeSVCCommonDriver, self).__init__(*args, **kwargs) super(StorwizeSVCCommonDriver, self).__init__(*args, **kwargs)
self.configuration.append_config_values(storwize_svc_opts) self.configuration.append_config_values(storwize_svc_opts)
@ -1724,6 +1850,23 @@ class StorwizeSVCCommonDriver(san.SanDriver,
'system_id': None, 'system_id': None,
'code_level': None, 'code_level': None,
} }
# Since there are three replication modes supported by Storwize,
# this dictionary is used to map the replication types to certain
# replications.
self.replications = {}
# One driver can be configured with multiple replication targets
# to failover.
self._replication_targets = []
# This boolean is used to indicate whether this driver is configured
# with replication.
self._replication_enabled = False
# This list is used to save the supported replication modes.
self._supported_replication_types = []
# Storwize has the limitation that can not burst more than 3 new ssh # Storwize has the limitation that can not burst more than 3 new ssh
# connections within 1 second. So slow down the initialization. # connections within 1 second. So slow down the initialization.
time.sleep(1) time.sleep(1)
@ -1778,6 +1921,9 @@ class StorwizeSVCCommonDriver(san.SanDriver,
self._check_volume_copy_ops) self._check_volume_copy_ops)
self._vdiskcopyops_loop.start(interval=self.VDISKCOPYOPS_INTERVAL) self._vdiskcopyops_loop.start(interval=self.VDISKCOPYOPS_INTERVAL)
# v2 replication setup
self._do_replication_setup()
def check_for_setup_error(self): def check_for_setup_error(self):
"""Ensure that the flags are set properly.""" """Ensure that the flags are set properly."""
LOG.debug('enter: check_for_setup_error') LOG.debug('enter: check_for_setup_error')
@ -1847,12 +1993,28 @@ class StorwizeSVCCommonDriver(san.SanDriver,
self._helpers.add_vdisk_qos(volume['name'], opts['qos']) self._helpers.add_vdisk_qos(volume['name'], opts['qos'])
model_update = None model_update = None
if opts.get('replication'):
ctxt = context.get_admin_context() ctxt = context.get_admin_context()
rep_type = self._get_volume_replicated_type(ctxt, volume)
# The replication V2 has a higher priority than the replication V1.
# Check if V2 is available first, then check if V1 is available.
if rep_type:
self.replications.get(rep_type).volume_replication_setup(ctxt,
volume)
model_update = {'replication_status': 'enabled'}
elif opts.get('replication'):
model_update = self.replication.create_replica(ctxt, volume) model_update = self.replication.create_replica(ctxt, volume)
return model_update return model_update
def delete_volume(self, volume): def delete_volume(self, volume):
ctxt = context.get_admin_context()
rep_mirror_type = self._get_volume_replicated_type_mirror(ctxt,
volume)
rep_status = volume.get("replication_status", None)
if rep_mirror_type and rep_status != "failed-over":
self.replications.get(rep_mirror_type).delete_target_volume(
volume)
self._helpers.delete_vdisk(volume['name'], False) self._helpers.delete_vdisk(volume['name'], False)
if volume['id'] in self._vdiskcopyops: if volume['id'] in self._vdiskcopyops:
@ -1894,8 +2056,16 @@ class StorwizeSVCCommonDriver(san.SanDriver,
if opts['qos']: if opts['qos']:
self._helpers.add_vdisk_qos(volume['name'], opts['qos']) self._helpers.add_vdisk_qos(volume['name'], opts['qos'])
if 'replication' in opts and opts['replication']:
ctxt = context.get_admin_context() ctxt = context.get_admin_context()
rep_type = self._get_volume_replicated_type(ctxt, volume)
# The replication V2 has a higher priority than the replication V1.
# Check if V2 is available first, then check if V1 is available.
if rep_type and self._replication_enabled:
self.replications.get(rep_type).volume_replication_setup(ctxt,
volume)
return {'replication_status': 'enabled'}
elif opts.get('replication'):
replica_status = self.replication.create_replica(ctxt, volume) replica_status = self.replication.create_replica(ctxt, volume)
if replica_status: if replica_status:
return replica_status return replica_status
@ -1916,8 +2086,16 @@ class StorwizeSVCCommonDriver(san.SanDriver,
if opts['qos']: if opts['qos']:
self._helpers.add_vdisk_qos(tgt_volume['name'], opts['qos']) self._helpers.add_vdisk_qos(tgt_volume['name'], opts['qos'])
if 'replication' in opts and opts['replication']:
ctxt = context.get_admin_context() ctxt = context.get_admin_context()
rep_type = self._get_volume_replicated_type(ctxt, tgt_volume)
# The replication V2 has a higher priority than the replication V1.
# Check if V2 is available first, then check if V1 is available.
if rep_type and self._replication_enabled:
self.replications.get(rep_type).volume_replication_setup(
ctxt, tgt_volume)
return {'replication_status': 'enabled'}
elif opts.get('replication'):
replica_status = self.replication.create_replica(ctxt, tgt_volume) replica_status = self.replication.create_replica(ctxt, tgt_volume)
if replica_status: if replica_status:
return replica_status return replica_status
@ -1933,7 +2111,32 @@ class StorwizeSVCCommonDriver(san.SanDriver,
raise exception.VolumeDriverException(message=msg) raise exception.VolumeDriverException(message=msg)
extend_amt = int(new_size) - volume['size'] extend_amt = int(new_size) - volume['size']
ctxt = context.get_admin_context()
rep_mirror_type = self._get_volume_replicated_type_mirror(ctxt,
volume)
rep_status = volume.get("replication_status", None)
target_vol_name = None
if rep_mirror_type and rep_status != "failed-over":
try:
rel_info = self._helpers.get_relationship_info(volume)
self._helpers.delete_relationship(volume)
except Exception as e:
msg = (_('Failed to get remote copy information for '
'%(volume)s. Exception: %(err)s.'), {'volume':
volume['id'],
'err': e})
LOG.error(msg)
raise exception.VolumeDriverException(message=msg)
if rel_info:
target_vol_name = rel_info.get('aux_vdisk_name')
self.replications.get(rep_mirror_type).extend_target_volume(
target_vol_name, extend_amt)
self._helpers.extend_vdisk(volume['name'], extend_amt) self._helpers.extend_vdisk(volume['name'], extend_amt)
if rep_mirror_type and rep_status != "failed-over":
self.replications.get(rep_mirror_type).create_relationship(
volume, target_vol_name)
LOG.debug('leave: extend_volume: volume %s', volume['id']) LOG.debug('leave: extend_volume: volume %s', volume['id'])
def add_vdisk_copy(self, volume, dest_pool, vol_type): def add_vdisk_copy(self, volume, dest_pool, vol_type):
@ -2068,6 +2271,165 @@ class StorwizeSVCCommonDriver(san.SanDriver,
copy_op[1]) copy_op[1])
LOG.debug("Exit: update volume copy status.") LOG.debug("Exit: update volume copy status.")
# #### V2 replication methods #### #
def replication_enable(self, context, vref):
"""Enable replication on a replication capable volume."""
rep_type = self._validate_volume_rep_type(context, vref)
if rep_type not in self.replications:
msg = _("Driver does not support re-enabling replication for a "
"failed over volume.")
LOG.error(msg)
raise exception.ReplicationError(volume_id=vref['id'],
reason=msg)
return self.replications.get(rep_type).replication_enable(
context, vref)
def replication_disable(self, context, vref):
"""Disable replication on a replication capable volume."""
rep_type = self._validate_volume_rep_type(context, vref)
return self.replications[rep_type].replication_disable(
context, vref)
def replication_failover(self, context, vref, secondary):
"""Force failover to a secondary replication target."""
rep_type = self._validate_volume_rep_type(context, vref)
return self.replications[rep_type].replication_failover(
context, vref, secondary)
def list_replication_targets(self, context, vref):
"""Return the list of replication targets for a volume."""
rep_type = self._validate_volume_rep_type(context, vref)
# When a volume is failed over, the secondary volume driver will not
# have replication configured, so in this case, gracefully handle
# request by returning no target volumes
if rep_type not in self.replications:
targets = []
else:
targets = self.replications[rep_type].list_replication_targets(
context, vref)
return {'volume_id': vref['id'],
'targets': targets}
def _validate_volume_rep_type(self, ctxt, volume):
rep_type = self._get_volume_replicated_type(ctxt, volume)
if not rep_type:
msg = (_("Volume %s is not of replicated type. "
"This volume needs to be of a volume type "
"with the extra spec replication_enabled set "
"to '<is> True' to support replication "
"actions."), volume['id'])
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
if not self._replication_enabled:
msg = _("The back-end where the volume is created "
"does not have replication enabled.")
LOG.error(msg)
raise exception.VolumeBackendAPIException(data=msg)
return rep_type
def _get_volume_replicated_type_mirror(self, ctxt, volume):
rep_type = self._get_volume_replicated_type(ctxt, volume)
if rep_type in self.VALID_REP_TYPES:
return rep_type
else:
return None
def _get_specs_replicated_type(self, volume_type):
replication_type = None
extra_specs = volume_type.get("extra_specs", {})
rep_val = extra_specs.get('replication_enabled')
if rep_val == "<is> True":
replication_type = extra_specs.get('replication_type',
self.GLOBAL)
# The format for replication_type in extra spec is in
# "<in> global". Otherwise, the code will
# not reach here.
if replication_type != self.GLOBAL:
# Pick up the replication type specified in the
# extra spec from the format like "<in> global".
replication_type = replication_type.split()[1]
if replication_type not in self.VALID_REP_TYPES:
replication_type = None
return replication_type
def _get_volume_replicated_type(self, ctxt, volume):
replication_type = None
if volume.get("volume_type_id"):
volume_type = volume_types.get_volume_type(
ctxt, volume["volume_type_id"])
replication_type = self._get_specs_replicated_type(volume_type)
return replication_type
def _do_replication_setup(self):
replication_devices = self.configuration.replication_device
if replication_devices:
replication_targets = []
for dev in replication_devices:
remote_array = {}
remote_array['managed_backend_name'] = (
dev.get('managed_backend_name'))
if not remote_array['managed_backend_name']:
raise exception.InvalidConfigurationValue(
option='managed_backend_name',
value=remote_array['managed_backend_name'])
rep_mode = dev.get('replication_mode')
remote_array['replication_mode'] = rep_mode
remote_array['san_ip'] = (
dev.get('san_ip'))
remote_array['target_device_id'] = (
dev.get('target_device_id'))
remote_array['san_login'] = (
dev.get('san_login'))
remote_array['san_password'] = (
dev.get('san_password'))
remote_array['pool_name'] = (
dev.get('pool_name'))
replication_targets.append(remote_array)
# Each replication type will have a coresponding replication.
self.create_replication_types(replication_targets)
if len(self._supported_replication_types) > 0:
self._replication_enabled = True
def create_replication_types(self, replication_targets):
for target in replication_targets:
rep_type = target['replication_mode']
if (rep_type in self.VALID_REP_TYPES
and rep_type not in self.replications.keys()):
replication = self.replication_factory(rep_type, target)
try:
replication.establish_target_partnership()
except exception.VolumeDriverException:
msg = (_LE('The replication mode of %(type)s has not '
'successfully established partnership '
'with the replica Storwize target %(stor)s.'),
{'type': rep_type,
'stor': target['target_device_id']})
LOG.error(msg)
continue
self.replications[rep_type] = replication
self._replication_targets.append(target)
self._supported_replication_types.append(rep_type)
def replication_factory(self, replication_type, rep_target):
"""Use replication methods for the requested mode."""
if replication_type == self.GLOBAL:
return storwize_rep.StorwizeSVCReplicationGlobalMirror(
self, rep_target, StorwizeHelpers)
if replication_type == self.METRO:
return storwize_rep.StorwizeSVCReplicationMetroMirror(
self, rep_target, StorwizeHelpers)
def get_replication_updates(self, context):
# TODO(vhou): the manager does not need to do anything so far.
replication_updates = []
return replication_updates
def migrate_volume(self, ctxt, volume, host): def migrate_volume(self, ctxt, volume, host):
"""Migrate directly if source and dest are managed by same storage. """Migrate directly if source and dest are managed by same storage.
@ -2474,7 +2836,11 @@ class StorwizeSVCCommonDriver(san.SanDriver,
{'sys_id': self._state['system_id'], {'sys_id': self._state['system_id'],
'pool': pool}) 'pool': pool})
if self.replication: if self._replication_enabled:
data['replication_enabled'] = self._replication_enabled
data['replication_type'] = self._supported_replication_types
data['replication_count'] = len(self._replication_targets)
elif self.replication:
data.update(self.replication.get_replication_info()) data.update(self.replication.get_replication_info())
self._stats = data self._stats = data

View File

@ -0,0 +1,3 @@
---
features:
- Adds managed v2 replication global and metro mirror modes support to the IBM Storwize driver.