Merge "libvirt:Rsync remote FS driver was added"

This commit is contained in:
Jenkins
2015-08-10 10:03:25 +00:00
committed by Gerrit Code Review
6 changed files with 427 additions and 78 deletions

View File

@@ -9999,14 +9999,20 @@ class LibvirtConnTestCase(test.NoDBTestCase):
drvr = libvirt_driver.LibvirtDriver(fake.FakeVirtAPI(), True)
drvr.get_host_ip_addr = mock.MagicMock(return_value='bar')
mock_exists.return_value = is_same
with mock.patch('nova.utils.ssh_execute') as mock_ssh_method:
result = drvr._is_storage_shared_with('foo', '/path')
mock_ssh_method.assert_any_call('foo', 'touch', mock.ANY)
with contextlib.nested(
mock.patch.object(drvr._remotefs, 'create_file'),
mock.patch.object(drvr._remotefs, 'remove_file')
) as (mock_rem_fs_create, mock_rem_fs_remove):
result = drvr._is_storage_shared_with('host', '/path')
mock_rem_fs_create.assert_any_call('host', mock.ANY)
create_args, create_kwargs = mock_rem_fs_create.call_args
self.assertTrue(create_args[1].startswith('/path'))
if is_same:
mock_unlink.assert_called_once_with(mock.ANY)
else:
self.assertEqual(2, mock_ssh_method.call_count)
mock_ssh_method.assert_called_with('foo', 'rm', mock.ANY)
mock_rem_fs_remove.assert_called_with('host', mock.ANY)
remove_args, remove_kwargs = mock_rem_fs_remove.call_args
self.assertTrue(remove_args[1].startswith('/path'))
return result
def test_shared_storage_detection_same_host(self):

View File

@@ -57,65 +57,23 @@ blah BLAH: bb
self.assertEqual('raw', disk_type)
@mock.patch('nova.utils.execute')
def test_copy_image_local_cp(self, mock_execute):
def test_copy_image_local(self, mock_execute):
libvirt_utils.copy_image('src', 'dest')
mock_execute.assert_called_once_with('cp', 'src', 'dest')
_rsync_call = functools.partial(mock.call,
'rsync', '--sparse', '--compress',
on_execute=None, on_completion=None)
@mock.patch('nova.utils.execute')
def test_copy_image_rsync(self, mock_execute):
@mock.patch('nova.virt.libvirt.volume.remotefs.SshDriver.copy_file')
def test_copy_image_remote_ssh(self, mock_rem_fs_remove):
self.flags(remote_filesystem_transport='ssh', group='libvirt')
libvirt_utils.copy_image('src', 'dest', host='host')
mock_rem_fs_remove.assert_called_once_with('src', 'host:dest',
on_completion=None, on_execute=None)
mock_execute.assert_has_calls([
self._rsync_call('--dry-run', 'src', 'host:dest'),
self._rsync_call('src', 'host:dest'),
])
self.assertEqual(2, mock_execute.call_count)
@mock.patch('nova.utils.execute')
def test_copy_image_scp(self, mock_execute):
mock_execute.side_effect = [
processutils.ProcessExecutionError,
mock.DEFAULT,
]
@mock.patch('nova.virt.libvirt.volume.remotefs.RsyncDriver.copy_file')
def test_copy_image_remote_rsync(self, mock_rem_fs_remove):
self.flags(remote_filesystem_transport='rsync', group='libvirt')
libvirt_utils.copy_image('src', 'dest', host='host')
mock_execute.assert_has_calls([
self._rsync_call('--dry-run', 'src', 'host:dest'),
mock.call('scp', 'src', 'host:dest',
on_execute=None, on_completion=None),
])
self.assertEqual(2, mock_execute.call_count)
@mock.patch('nova.utils.execute')
def test_copy_image_rsync_ipv6(self, mock_execute):
libvirt_utils.copy_image('src', 'dest', host='2600::')
mock_execute.assert_has_calls([
self._rsync_call('--dry-run', 'src', '[2600::]:dest'),
self._rsync_call('src', '[2600::]:dest'),
])
self.assertEqual(2, mock_execute.call_count)
@mock.patch('nova.utils.execute')
def test_copy_image_scp_ipv6(self, mock_execute):
mock_execute.side_effect = [
processutils.ProcessExecutionError,
mock.DEFAULT,
]
libvirt_utils.copy_image('src', 'dest', host='2600::')
mock_execute.assert_has_calls([
self._rsync_call('--dry-run', 'src', '[2600::]:dest'),
mock.call('scp', 'src', '[2600::]:dest',
on_execute=None, on_completion=None),
])
self.assertEqual(2, mock_execute.call_count)
mock_rem_fs_remove.assert_called_once_with('src', 'host:dest',
on_completion=None, on_execute=None)
@mock.patch('os.path.exists', return_value=True)
def test_disk_type(self, mock_exists):

View File

@@ -58,3 +58,125 @@ class RemoteFSTestCase(test.NoDBTestCase):
mock_execute.assert_any_call('umount', mock.sentinel.mount_path,
run_as_root=True, attempts=3,
delay_on_retry=True)
@mock.patch('tempfile.mkdtemp', return_value='/tmp/Mercury')
@mock.patch('nova.utils.execute')
def test_remove_remote_file_rsync(self, mock_execute, mock_mkdtemp):
remotefs.RsyncDriver().remove_file('host', 'dest', None, None)
rsync_call_args = mock.call('rsync', '--archive',
'--delete', '--include',
'dest', '--exclude', '*',
'/tmp/Mercury/', 'host:',
on_completion=None, on_execute=None)
self.assertEqual(mock_execute.mock_calls[0], rsync_call_args)
rm_call_args = mock.call('rm', '-rf', '/tmp/Mercury')
self.assertEqual(mock_execute.mock_calls[1], rm_call_args)
self.assertEqual(2, mock_execute.call_count)
self.assertEqual(1, mock_mkdtemp.call_count)
@mock.patch('nova.utils.execute')
def test_remove_remote_file_ssh(self, mock_execute):
remotefs.SshDriver().remove_file('host', 'dest', None, None)
mock_execute.assert_called_once_with(
'ssh', 'host', 'rm', 'dest',
on_completion=None, on_execute=None)
@mock.patch('tempfile.mkdtemp', return_value='/tmp/Venus')
@mock.patch('nova.utils.execute')
def test_remove_remote_dir_rsync(self, mock_execute, mock_mkdtemp):
remotefs.RsyncDriver().remove_dir('host', 'dest', None, None)
rsync_call_args = mock.call('rsync', '--archive',
'--delete-excluded', '/tmp/Venus/',
'host:dest',
on_completion=None, on_execute=None)
self.assertEqual(mock_execute.mock_calls[0], rsync_call_args)
rsync_call_args = mock.call('rsync', '--archive',
'--delete', '--include',
'dest', '--exclude', '*',
'/tmp/Venus/', 'host:',
on_completion=None, on_execute=None)
self.assertEqual(mock_execute.mock_calls[1], rsync_call_args)
rm_call_args = mock.call('rm', '-rf', '/tmp/Venus')
self.assertEqual(mock_execute.mock_calls[2], rm_call_args)
self.assertEqual(3, mock_execute.call_count)
self.assertEqual(1, mock_mkdtemp.call_count)
@mock.patch('nova.utils.execute')
def test_remove_remote_dir_ssh(self, mock_execute):
remotefs.SshDriver().remove_dir('host', 'dest', None, None)
mock_execute.assert_called_once_with(
'ssh', 'host', 'rm', '-rf', 'dest', on_completion=None,
on_execute=None)
@mock.patch('tempfile.mkdtemp', return_value='/tmp/Mars')
@mock.patch('nova.utils.execute')
def test_create_remote_file_rsync(self, mock_execute, mock_mkdtemp):
remotefs.RsyncDriver().create_file('host', 'dest_dir', None, None)
mkdir_call_args = mock.call('mkdir', '-p', '/tmp/Mars/',
on_completion=None, on_execute=None)
self.assertEqual(mock_execute.mock_calls[0], mkdir_call_args)
touch_call_args = mock.call('touch', '/tmp/Mars/dest_dir',
on_completion=None, on_execute=None)
self.assertEqual(mock_execute.mock_calls[1], touch_call_args)
rsync_call_args = mock.call('rsync', '--archive', '--relative',
'--no-implied-dirs',
'/tmp/Mars/./dest_dir', 'host:/',
on_completion=None, on_execute=None)
self.assertEqual(mock_execute.mock_calls[2], rsync_call_args)
rm_call_args = mock.call('rm', '-rf', '/tmp/Mars')
self.assertEqual(mock_execute.mock_calls[3], rm_call_args)
self.assertEqual(4, mock_execute.call_count)
self.assertEqual(1, mock_mkdtemp.call_count)
@mock.patch('nova.utils.execute')
def test_create_remote_file_ssh(self, mock_execute):
remotefs.SshDriver().create_file('host', 'dest_dir', None, None)
mock_execute.assert_called_once_with('ssh', 'host',
'touch', 'dest_dir',
on_completion=None,
on_execute=None)
@mock.patch('tempfile.mkdtemp', return_value='/tmp/Jupiter')
@mock.patch('nova.utils.execute')
def test_create_remote_dir_rsync(self, mock_execute, mock_mkdtemp):
remotefs.RsyncDriver().create_dir('host', 'dest_dir', None, None)
mkdir_call_args = mock.call('mkdir', '-p', '/tmp/Jupiter/dest_dir',
on_completion=None, on_execute=None)
self.assertEqual(mock_execute.mock_calls[0], mkdir_call_args)
rsync_call_args = mock.call('rsync', '--archive', '--relative',
'--no-implied-dirs',
'/tmp/Jupiter/./dest_dir', 'host:/',
on_completion=None, on_execute=None)
self.assertEqual(mock_execute.mock_calls[1], rsync_call_args)
rm_call_args = mock.call('rm', '-rf', '/tmp/Jupiter')
self.assertEqual(mock_execute.mock_calls[2], rm_call_args)
self.assertEqual(3, mock_execute.call_count)
self.assertEqual(1, mock_mkdtemp.call_count)
@mock.patch('nova.utils.execute')
def test_create_remote_dir_ssh(self, mock_execute):
remotefs.SshDriver().create_dir('host', 'dest_dir', None, None)
mock_execute.assert_called_once_with('ssh', 'host', 'mkdir',
'-p', 'dest_dir',
on_completion=None,
on_execute=None)
@mock.patch('nova.utils.execute')
def test_remote_copy_file_rsync(self, mock_execute):
remotefs.RsyncDriver().copy_file('1.2.3.4:/home/star_wars',
'/home/favourite', None, None)
mock_execute.assert_called_once_with('rsync', '--sparse', '--compress',
'1.2.3.4:/home/star_wars',
'/home/favourite',
on_completion=None,
on_execute=None)
@mock.patch('nova.utils.execute')
def test_remote_copy_file_ssh(self, mock_execute):
remotefs.SshDriver().copy_file('1.2.3.4:/home/SpaceOdyssey',
'/home/favourite', None, None)
mock_execute.assert_called_once_with('scp',
'1.2.3.4:/home/SpaceOdyssey',
'/home/favourite',
on_completion=None,
on_execute=None)

View File

@@ -103,6 +103,7 @@ from nova.virt.libvirt.storage import lvm
from nova.virt.libvirt.storage import rbd_utils
from nova.virt.libvirt import utils as libvirt_utils
from nova.virt.libvirt import vif as libvirt_vif
from nova.virt.libvirt.volume import remotefs
from nova.virt import netutils
from nova.virt import watchdog_actions
from nova import volume
@@ -477,6 +478,7 @@ class LibvirtDriver(driver.ComputeDriver):
sysinfo_serial_funcs.keys())})
self.job_tracker = instancejobtracker.InstanceJobTracker()
self._remotefs = remotefs.RemoteFilesystem()
def _get_volume_drivers(self):
return libvirt_volume_drivers
@@ -6270,7 +6272,7 @@ class LibvirtDriver(driver.ComputeDriver):
utils.execute('rm', '-rf', inst_base)
utils.execute('mv', inst_base_resize, inst_base)
if not shared_storage:
utils.ssh_execute(dest, 'rm', '-rf', inst_base)
self._remotefs.remove_dir(dest, inst_base)
except Exception:
pass
@@ -6285,12 +6287,12 @@ class LibvirtDriver(driver.ComputeDriver):
tmp_path = os.path.join(inst_base, tmp_file)
try:
utils.ssh_execute(dest, 'touch', tmp_path)
self._remotefs.create_file(dest, tmp_path)
if os.path.exists(tmp_path):
shared_storage = True
os.unlink(tmp_path)
else:
utils.ssh_execute(dest, 'rm', tmp_path)
self._remotefs.remove_file(dest, tmp_path)
except Exception:
pass
return shared_storage
@@ -6344,7 +6346,7 @@ class LibvirtDriver(driver.ComputeDriver):
# failures here earlier
if not shared_storage:
try:
utils.ssh_execute(dest, 'mkdir', '-p', inst_base)
self._remotefs.create_dir(dest, inst_base)
except processutils.ProcessExecutionError as e:
reason = _("not able to execute ssh command: %s") % e
raise exception.InstanceFaultRollback(

View File

@@ -33,6 +33,7 @@ from nova.i18n import _LI
from nova import utils
from nova.virt import images
from nova.virt.libvirt import config as vconfig
from nova.virt.libvirt.volume import remotefs
from nova.virt import volumeutils
libvirt_opts = [
@@ -206,22 +207,10 @@ def copy_image(src, dest, host=None, receive=False,
src = "%s:%s" % (utils.safe_ip_format(host), src)
else:
dest = "%s:%s" % (utils.safe_ip_format(host), dest)
# Try rsync first as that can compress and create sparse dest files.
# Note however that rsync currently doesn't read sparse files
# efficiently: https://bugzilla.samba.org/show_bug.cgi?id=8918
# At least network traffic is mitigated with compression.
try:
# Do a relatively light weight test first, so that we
# can fall back to scp, without having run out of space
# on the destination for example.
execute('rsync', '--sparse', '--compress', '--dry-run', src, dest,
on_execute=on_execute, on_completion=on_completion)
except processutils.ProcessExecutionError:
execute('scp', src, dest, on_execute=on_execute,
on_completion=on_completion)
else:
execute('rsync', '--sparse', '--compress', src, dest,
on_execute=on_execute, on_completion=on_completion)
remote_filesystem_driver = remotefs.RemoteFilesystem()
remote_filesystem_driver.copy_file(src, dest,
on_execute=on_execute, on_completion=on_completion)
def write_to_file(path, contents, umask=None):

View File

@@ -13,14 +13,33 @@
# License for the specific language governing permissions and limitations
# under the License.
import abc
import functools
import os
import tempfile
from oslo_concurrency import processutils
from oslo_config import cfg
from oslo_log import log as logging
from oslo_utils import importutils
import six
from nova.i18n import _LE, _LW
from nova import utils
LOG = logging.getLogger(__name__)
libvirt_opts = [
cfg.StrOpt('remote_filesystem_transport',
default='ssh',
choices=('ssh', 'rsync'),
help='Use ssh or rsync transport for creating, copying, '
'removing files on the remote host.'),
]
CONF = cfg.CONF
CONF.register_opts(libvirt_opts, 'libvirt')
def mount_share(mount_path, export_path,
export_type, options=None):
@@ -62,3 +81,256 @@ def unmount_share(mount_path, export_path):
else:
LOG.exception(_LE("Couldn't unmount the share %s"),
export_path)
class RemoteFilesystem(object):
"""Represents actions that can be taken on a remote host's filesystem."""
def __init__(self):
transport = CONF.libvirt.remote_filesystem_transport
cls_name = '.'.join([__name__, transport.capitalize()])
cls_name += 'Driver'
self.driver = importutils.import_object(cls_name)
def create_file(self, host, dst_path, on_execute=None,
on_completion=None):
LOG.debug("Creating file %s on remote host %s", dst_path, host)
self.driver.create_file(host, dst_path, on_execute=on_execute,
on_completion=on_completion)
def remove_file(self, host, dst_path, on_execute=None,
on_completion=None):
LOG.debug("Removing file %s on remote host %s", dst_path, host)
self.driver.remove_file(host, dst_path, on_execute=on_execute,
on_completion=on_completion)
def create_dir(self, host, dst_path, on_execute=None,
on_completion=None):
LOG.debug("Creating directory %s on remote host %s", dst_path, host)
self.driver.create_dir(host, dst_path, on_execute=on_execute,
on_completion=on_completion)
def remove_dir(self, host, dst_path, on_execute=None,
on_completion=None):
LOG.debug("Removing directory %s on remote host %s", dst_path, host)
self.driver.remove_dir(host, dst_path, on_execute=on_execute,
on_completion=on_completion)
def copy_file(self, src, dst, on_execute=None,
on_completion=None):
LOG.debug("Copying file %s to %s", src, dst)
self.driver.copy_file(src, dst, on_execute=on_execute,
on_completion=on_completion)
@six.add_metaclass(abc.ABCMeta)
class RemoteFilesystemDriver(object):
@abc.abstractmethod
def create_file(self, host, dst_path, on_execute, on_completion):
"""Create file on the remote system.
:param host: Remote host
:param dst_path: Destination path
:param on_execute: Callback method to store pid of process in cache
:param on_completion: Callback method to remove pid of process from
cache
"""
@abc.abstractmethod
def remove_file(self, host, dst_path, on_execute, on_completion):
"""Removes a file on a remote host.
:param host: Remote host
:param dst_path: Destination path
:param on_execute: Callback method to store pid of process in cache
:param on_completion: Callback method to remove pid of process from
cache
"""
@abc.abstractmethod
def create_dir(self, host, dst_path, on_execute, on_completion):
"""Create directory on the remote system.
:param host: Remote host
:param dst_path: Destination path
:param on_execute: Callback method to store pid of process in cache
:param on_completion: Callback method to remove pid of process from
cache
"""
@abc.abstractmethod
def remove_dir(self, host, dst_path, on_execute, on_completion):
"""Removes a directory on a remote host.
:param host: Remote host
:param dst_path: Destination path
:param on_execute: Callback method to store pid of process in cache
:param on_completion: Callback method to remove pid of process from
cache
"""
@abc.abstractmethod
def copy_file(self, src, dst, on_execute, on_completion):
"""Copy file to/from remote host.
Remote address must be specified in format:
REM_HOST_IP_ADDRESS:REM_HOST_PATH
For example:
192.168.1.10:/home/file
:param src: Source address
:param dst: Destination path
:param on_execute: Callback method to store pid of process in cache
:param on_completion: Callback method to remove pid of process from
"""
class SshDriver(RemoteFilesystemDriver):
def create_file(self, host, dst_path, on_execute, on_completion):
utils.execute('ssh', host, 'touch', dst_path,
on_execute=on_execute, on_completion=on_completion)
def remove_file(self, host, dst, on_execute, on_completion):
utils.execute('ssh', host, 'rm', dst,
on_execute=on_execute, on_completion=on_completion)
def create_dir(self, host, dst_path, on_execute, on_completion):
utils.execute('ssh', host, 'mkdir', '-p', dst_path,
on_execute=on_execute, on_completion=on_completion)
def remove_dir(self, host, dst, on_execute, on_completion):
utils.execute('ssh', host, 'rm', '-rf', dst,
on_execute=on_execute, on_completion=on_completion)
def copy_file(self, src, dst, on_execute, on_completion):
utils.execute('scp', src, dst,
on_execute=on_execute, on_completion=on_completion)
def create_tmp_dir(function):
"""Creates temporary directory for rsync purposes.
Removes created directory in the end.
"""
@functools.wraps(function)
def decorated_function(*args, **kwargs):
# Create directory
tmp_dir_path = tempfile.mkdtemp()
kwargs['tmp_dir_path'] = tmp_dir_path
try:
return function(*args, **kwargs)
finally:
# Remove directory
utils.execute('rm', '-rf', tmp_dir_path)
return decorated_function
class RsyncDriver(RemoteFilesystemDriver):
@create_tmp_dir
def create_file(self, host, dst_path, on_execute, on_completion, **kwargs):
dir_path = os.path.dirname(os.path.normpath(dst_path))
# Create target dir inside temporary directory
local_tmp_dir = os.path.join(kwargs['tmp_dir_path'],
dir_path.strip(os.path.sep))
utils.execute('mkdir', '-p', local_tmp_dir,
on_execute=on_execute, on_completion=on_completion)
# Create file in directory
file_name = os.path.basename(os.path.normpath(dst_path))
local_tmp_file = os.path.join(local_tmp_dir, file_name)
utils.execute('touch', local_tmp_file,
on_execute=on_execute, on_completion=on_completion)
RsyncDriver._synchronize_object(kwargs['tmp_dir_path'],
host, dst_path,
on_execute=on_execute,
on_completion=on_completion)
@create_tmp_dir
def remove_file(self, host, dst, on_execute, on_completion, **kwargs):
# Delete file
RsyncDriver._remove_object(kwargs['tmp_dir_path'], host, dst,
on_execute=on_execute,
on_completion=on_completion)
@create_tmp_dir
def create_dir(self, host, dst_path, on_execute, on_completion, **kwargs):
dir_path = os.path.normpath(dst_path)
# Create target dir inside temporary directory
local_tmp_dir = os.path.join(kwargs['tmp_dir_path'],
dir_path.strip(os.path.sep))
utils.execute('mkdir', '-p', local_tmp_dir,
on_execute=on_execute, on_completion=on_completion)
RsyncDriver._synchronize_object(kwargs['tmp_dir_path'],
host, dst_path,
on_execute=on_execute,
on_completion=on_completion)
@create_tmp_dir
def remove_dir(self, host, dst, on_execute, on_completion, **kwargs):
# Remove remote directory's content
utils.execute('rsync', '--archive', '--delete-excluded',
kwargs['tmp_dir_path'] + os.path.sep,
'%s:%s' % (host, dst),
on_execute=on_execute, on_completion=on_completion)
# Delete empty directory
RsyncDriver._remove_object(kwargs['tmp_dir_path'], host, dst,
on_execute=on_execute,
on_completion=on_completion)
@staticmethod
def _remove_object(src, host, dst, on_execute, on_completion):
"""Removes a file or empty directory on a remote host.
:param src: Empty directory used for rsync purposes
:param host: Remote host
:param dst: Destination path
:param on_execute: Callback method to store pid of process in cache
:param on_completion: Callback method to remove pid of process from
cache
"""
utils.execute('rsync', '--archive', '--delete',
'--include', os.path.basename(os.path.normpath(dst)),
'--exclude', '*',
os.path.normpath(src) + os.path.sep,
'%s:%s' % (host, os.path.dirname(os.path.normpath(dst))),
on_execute=on_execute, on_completion=on_completion)
@staticmethod
def _synchronize_object(src, host, dst, on_execute, on_completion):
"""Creates a file or empty directory on a remote host.
:param src: Empty directory used for rsync purposes
:param host: Remote host
:param dst: Destination path
:param on_execute: Callback method to store pid of process in cache
:param on_completion: Callback method to remove pid of process from
cache
"""
# For creating path on the remote host rsync --relative path must
# be used. With a modern rsync on the sending side (beginning with
# 2.6.7), you can insert a dot and a slash into the source path,
# like this:
# rsync -avR /foo/./bar/baz.c remote:/tmp/
# That would create /tmp/bar/baz.c on the remote machine.
# (Note that the dot must be followed by a slash, so "/foo/."
# would not be abbreviated.)
relative_tmp_file_path = os.path.join(
src, './',
os.path.normpath(dst).strip(os.path.sep))
# Do relative rsync local directory with remote root directory
utils.execute('rsync', '--archive', '--relative', '--no-implied-dirs',
relative_tmp_file_path, '%s:%s' % (host, os.path.sep),
on_execute=on_execute, on_completion=on_completion)
def copy_file(self, src, dst, on_execute, on_completion):
utils.execute('rsync', '--sparse', '--compress', src, dst,
on_execute=on_execute, on_completion=on_completion)