Add volume migration code to Nexenta iSCSI volume driver

Utilize ZFS-specific optimizations for direct host-to-host
volume migration

DocImpact
Implements: blueprint nexenta-iscsi-volume-migrate
Change-Id: Ib9f2fa7e1ccb93dbdbc951ddc3a0eda55cb001d5
This commit is contained in:
keystone 2013-11-11 09:21:30 -08:00 committed by Jay S. Bryant
parent 79d8072236
commit a0cbbf8e1a
5 changed files with 172 additions and 3 deletions

View File

@ -43,12 +43,14 @@ class TestNexentaISCSIDriver(test.TestCase):
TEST_VOLUME_REF = {
'name': TEST_VOLUME_NAME,
'size': 1,
'id': '1'
'id': '1',
'status': 'available'
}
TEST_VOLUME_REF2 = {
'name': TEST_VOLUME_NAME2,
'size': 1,
'id': '2'
'id': '2',
'status': 'in-use'
}
TEST_SNAPSHOT_REF = {
'name': TEST_SNAPSHOT_NAME,
@ -72,6 +74,9 @@ class TestNexentaISCSIDriver(test.TestCase):
self.configuration.nexenta_target_group_prefix = 'cinder/'
self.configuration.nexenta_blocksize = '8K'
self.configuration.nexenta_sparse = True
self.configuration.nexenta_rrmgr_compression = 1
self.configuration.nexenta_rrmgr_tcp_buf_size = 1024
self.configuration.nexenta_rrmgr_connections = 2
self.nms_mock = self.mox.CreateMockAnything()
for mod in ['volume', 'zvol', 'iscsitarget', 'appliance',
'stmf', 'scsidisk', 'snapshot']:
@ -150,6 +155,43 @@ class TestNexentaISCSIDriver(test.TestCase):
self.mox.ReplayAll()
self.drv.create_cloned_volume(vol, src_vref)
def test_migrate_volume(self):
volume = self.TEST_VOLUME_REF
host = {
'capabilities': {
'vendor_name': 'Nexenta',
'location_info': 'NexentaISCSIDriver:1.1.1.1:cinder',
'free_capacity_gb': 1
}
}
snapshot = {
'volume_name': volume['name'],
'name': 'cinder-migrate-snapshot-%s' % volume['id'],
}
self.nms_mock.appliance.ssh_list_bindings().AndReturn([])
self.nms_mock.zvol.create_snapshot('cinder/%s' % volume['name'],
snapshot['name'], '')
src = '%(volume)s/%(zvol)s@%(snapshot)s' % {
'volume': 'cinder',
'zvol': volume['name'],
'snapshot': snapshot['name']}
dst = '1.1.1.1:cinder'
cmd = ' '.join(['rrmgr -s zfs -c 1 -q -e -w 1024 -n 2', src, dst])
self.nms_mock.appliance.execute(cmd)
self.nms_mock.snapshot.destroy('cinder/%(volume)s@%(snapshot)s' % {
'volume': volume['name'],
'snapshot': snapshot['name']}, '')
volume_name = 'cinder/%s' % volume['name']
self.nms_mock.zvol.get_child_props(volume_name,
'origin').AndReturn(None)
self.nms_mock.zvol.destroy(volume_name, '')
self.mox.ReplayAll()
self.drv.migrate_volume(None, volume, host)
def test_create_snapshot(self):
self.nms_mock.zvol.create_snapshot('cinder/volume1', 'snapshot1', '')
self.mox.ReplayAll()

View File

@ -69,12 +69,17 @@ class NexentaISCSIDriver(driver.ISCSIDriver): # pylint: disable=R0921
options.NEXENTA_ISCSI_OPTIONS)
self.configuration.append_config_values(
options.NEXENTA_VOLUME_OPTIONS)
self.configuration.append_config_values(
options.NEXENTA_RRMGR_OPTIONS)
self.nms_protocol = self.configuration.nexenta_rest_protocol
self.nms_host = self.configuration.nexenta_host
self.nms_port = self.configuration.nexenta_rest_port
self.nms_user = self.configuration.nexenta_user
self.nms_password = self.configuration.nexenta_password
self.volume = self.configuration.nexenta_volume
self.rrmgr_compression = self.configuration.nexenta_rrmgr_compression
self.rrmgr_tcp_buf_size = self.configuration.nexenta_rrmgr_tcp_buf_size
self.rrmgr_connections = self.configuration.nexenta_rrmgr_connections
@property
def backend_name(self):
@ -127,6 +132,11 @@ class NexentaISCSIDriver(driver.ISCSIDriver): # pylint: disable=R0921
name = snapshot.split('@')[-1]
return name.startswith('cinder-clone-snapshot-')
@staticmethod
def _get_migrate_snapshot_name(volume):
"""Return name for snapshot that will be used to migrate the volume."""
return 'cinder-migrate-snapshot-%(id)s' % volume
def create_volume(self, volume):
"""Create a zvol on appliance.
@ -205,6 +215,84 @@ class NexentaISCSIDriver(driver.ISCSIDriver): # pylint: disable=R0921
'%(volume_name)s@%(name)s'), snapshot)
raise
def _get_zfs_send_recv_cmd(self, src, dst):
"""Returns rrmgr command for source and destination."""
return utils.get_rrmgr_cmd(src, dst,
compression=self.rrmgr_compression,
tcp_buf_size=self.rrmgr_tcp_buf_size,
connections=self.rrmgr_connections)
def migrate_volume(self, ctxt, volume, host):
"""Migrate if volume and host are managed by Nexenta appliance.
:param ctxt: context
:param volume: a dictionary describing the volume to migrate
:param host: a dictionary describing the host to migrate to
"""
LOG.debug(_('Enter: migrate_volume: id=%(id)s, host=%(host)s') %
{'id': volume['id'], 'host': host})
false_ret = (False, None)
if volume['status'] != 'available':
return false_ret
if 'location_info' not in host['capabilities']:
return false_ret
dst_parts = host['capabilities']['location_info'].split(':')
if host['capabilities']['vendor_name'] != 'Nexenta' or \
dst_parts[0] != self.__class__.__name__ or \
host['capabilities']['free_capacity_gb'] < volume['size']:
return false_ret
dst_host, dst_volume = dst_parts[1:]
ssh_bound = False
ssh_bindings = self.nms.appliance.ssh_list_bindings()
for bind in ssh_bindings:
if bind.index(dst_host) != -1:
ssh_bound = True
break
if not(ssh_bound):
LOG.warning(_("Remote NexentaStor appliance at %s should be "
"SSH-bound."), dst_host)
# Create temporary snapshot of volume on NexentaStor Appliance.
snapshot = {'volume_name': volume['name'],
'name': self._get_migrate_snapshot_name(volume)}
self.create_snapshot(snapshot)
src = '%(volume)s/%(zvol)s@%(snapshot)s' % {
'volume': self.volume,
'zvol': volume['name'],
'snapshot': snapshot['name']}
dst = ':'.join([dst_host, dst_volume])
try:
self.nms.appliance.execute(self._get_zfs_send_recv_cmd(src, dst))
except nexenta.NexentaException as exc:
LOG.warning(_("Cannot send source snapshot %(src)s to "
"destination %(dst)s. Reason: %(exc)s"),
{'src': src, 'dst': dst, 'exc': exc})
return false_ret
finally:
try:
self.delete_snapshot(snapshot)
except nexenta.NexentaException as exc:
LOG.warning(_("Cannot delete temporary source snapshot "
"%(src)s on NexentaStor Appliance: %(exc)s"),
{'src': src, 'exc': exc})
try:
self.delete_volume(volume)
except nexenta.NexentaException as exc:
LOG.warning(_("Cannot delete source volume %(volume)s on "
"NexentaStor Appliance: %(exc)s"),
{'volume': volume['name'], 'exc': exc})
return (True, None)
def create_snapshot(self, snapshot):
"""Create snapshot of existing zvol on appliance.

View File

@ -103,3 +103,16 @@ NEXENTA_VOLUME_OPTIONS = [
default=False,
help='flag to create sparse volumes'),
]
NEXENTA_RRMGR_OPTIONS = [
cfg.IntOpt('nexenta_rrmgr_compression',
default=0,
help=('Enable stream compression, level 1..9. 1 - gives best '
'speed; 9 - gives best compression.')),
cfg.IntOpt('nexenta_rrmgr_tcp_buf_size',
default=4096,
help='TCP Buffer size in KiloBytes.'),
cfg.IntOpt('nexenta_rrmgr_connections',
default=2,
help='Number of TCP connections.'),
]

View File

@ -20,7 +20,7 @@
.. automodule:: nexenta.utils
.. moduleauthor:: Victor Rodionov <victor.rodionov@nexenta.com>
.. moduleauthor:: Mikhail Khodos <hodosmb@gmail.com>
.. moduleauthor:: Mikhail Khodos <mikhail.khodos@nexenta.com>
"""
import re
@ -63,6 +63,22 @@ def str2gib_size(s):
return size_in_bytes / units.GiB
def get_rrmgr_cmd(src, dst, compression=None, tcp_buf_size=None,
connections=None):
"""Returns rrmgr command for source and destination."""
cmd = ['rrmgr', '-s', 'zfs']
if compression:
cmd.extend(['-c', '%s' % str(compression)])
cmd.append('-q')
cmd.append('-e')
if tcp_buf_size:
cmd.extend(['-w', str(tcp_buf_size)])
if connections:
cmd.extend(['-n', str(connections)])
cmd.extend([src, dst])
return ' '.join(cmd)
def parse_nms_url(url):
"""Parse NMS url into normalized parts like scheme, user, host and others.

View File

@ -1377,6 +1377,16 @@
# value. (boolean value)
#nexenta_nms_cache_volroot=true
# Enable stream compression, level 1..9. 1 - gives best speed;
# 9 - gives best compression. (integer value)
#nexenta_rrmgr_compression=0
# TCP Buffer size in KiloBytes. (integer value)
#nexenta_rrmgr_tcp_buf_size=4096
# Number of TCP connections. (integer value)
#nexenta_rrmgr_connections=2
# block size for volumes (blank=default,8KB) (string value)
#nexenta_blocksize=