Merge "Add Windows volume backup support"
This commit is contained in:
commit
856e636079
@ -25,6 +25,7 @@ import abc
|
|||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
import eventlet
|
import eventlet
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
@ -41,6 +42,9 @@ from cinder import objects
|
|||||||
from cinder.objects import fields
|
from cinder.objects import fields
|
||||||
from cinder.volume import utils as volume_utils
|
from cinder.volume import utils as volume_utils
|
||||||
|
|
||||||
|
if sys.platform == 'win32':
|
||||||
|
from os_win import utilsfactory as os_win_utilsfactory
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
chunkedbackup_service_opts = [
|
chunkedbackup_service_opts = [
|
||||||
@ -116,6 +120,13 @@ class ChunkedBackupDriver(driver.BackupDriver):
|
|||||||
self._get_compressor(CONF.backup_compression_algorithm)
|
self._get_compressor(CONF.backup_compression_algorithm)
|
||||||
self.support_force_delete = True
|
self.support_force_delete = True
|
||||||
|
|
||||||
|
if sys.platform == 'win32' and self.chunk_size_bytes % 4096:
|
||||||
|
# The chunk size must be a multiple of the sector size. In order
|
||||||
|
# to fail out early and avoid attaching the disks, we'll just
|
||||||
|
# enforce the chunk size to be a multiple of 4096.
|
||||||
|
err = _("Invalid chunk size. It must be a multiple of 4096.")
|
||||||
|
raise exception.InvalidConfigurationValue(message=err)
|
||||||
|
|
||||||
def _get_object_writer(self, container, object_name, extra_metadata=None):
|
def _get_object_writer(self, container, object_name, extra_metadata=None):
|
||||||
"""Return writer proxy-wrapped to execute methods in native thread."""
|
"""Return writer proxy-wrapped to execute methods in native thread."""
|
||||||
writer = self.get_object_writer(container, object_name, extra_metadata)
|
writer = self.get_object_writer(container, object_name, extra_metadata)
|
||||||
@ -453,6 +464,12 @@ class ChunkedBackupDriver(driver.BackupDriver):
|
|||||||
extra_usage_info=
|
extra_usage_info=
|
||||||
object_meta)
|
object_meta)
|
||||||
|
|
||||||
|
def _get_win32_phys_disk_size(self, disk_path):
|
||||||
|
win32_diskutils = os_win_utilsfactory.get_diskutils()
|
||||||
|
disk_number = win32_diskutils.get_device_number_from_device_name(
|
||||||
|
disk_path)
|
||||||
|
return win32_diskutils.get_disk_size(disk_number)
|
||||||
|
|
||||||
def backup(self, backup, volume_file, backup_metadata=True):
|
def backup(self, backup, volume_file, backup_metadata=True):
|
||||||
"""Backup the given volume.
|
"""Backup the given volume.
|
||||||
|
|
||||||
@ -488,6 +505,13 @@ class ChunkedBackupDriver(driver.BackupDriver):
|
|||||||
'backup. Do a full backup.')
|
'backup. Do a full backup.')
|
||||||
raise exception.InvalidBackup(reason=err)
|
raise exception.InvalidBackup(reason=err)
|
||||||
|
|
||||||
|
if sys.platform == 'win32':
|
||||||
|
# When dealing with Windows physical disks, we need the exact
|
||||||
|
# size of the disk. Attempting to read passed this boundary will
|
||||||
|
# lead to an IOError exception. At the same time, we cannot
|
||||||
|
# seek to the end of file.
|
||||||
|
win32_disk_size = self._get_win32_phys_disk_size(volume_file.name)
|
||||||
|
|
||||||
(object_meta, object_sha256, extra_metadata, container,
|
(object_meta, object_sha256, extra_metadata, container,
|
||||||
volume_size_bytes) = self._prepare_backup(backup)
|
volume_size_bytes) = self._prepare_backup(backup)
|
||||||
|
|
||||||
@ -526,7 +550,14 @@ class ChunkedBackupDriver(driver.BackupDriver):
|
|||||||
LOG.debug('Cancel the backup process of %s.', backup.id)
|
LOG.debug('Cancel the backup process of %s.', backup.id)
|
||||||
break
|
break
|
||||||
data_offset = volume_file.tell()
|
data_offset = volume_file.tell()
|
||||||
data = volume_file.read(self.chunk_size_bytes)
|
|
||||||
|
if sys.platform == 'win32':
|
||||||
|
read_bytes = min(self.chunk_size_bytes,
|
||||||
|
win32_disk_size - data_offset)
|
||||||
|
else:
|
||||||
|
read_bytes = self.chunk_size_bytes
|
||||||
|
data = volume_file.read(read_bytes)
|
||||||
|
|
||||||
if data == b'':
|
if data == b'':
|
||||||
break
|
break
|
||||||
|
|
||||||
|
@ -275,6 +275,10 @@ class BackupManager(manager.ThreadPoolManager):
|
|||||||
try:
|
try:
|
||||||
temp_snapshot = objects.Snapshot.get_by_id(
|
temp_snapshot = objects.Snapshot.get_by_id(
|
||||||
ctxt, backup.temp_snapshot_id)
|
ctxt, backup.temp_snapshot_id)
|
||||||
|
# We may want to consider routing those calls through the
|
||||||
|
# cinder API.
|
||||||
|
temp_snapshot.status = fields.SnapshotStatus.DELETING
|
||||||
|
temp_snapshot.save()
|
||||||
self.volume_rpcapi.delete_snapshot(ctxt, temp_snapshot)
|
self.volume_rpcapi.delete_snapshot(ctxt, temp_snapshot)
|
||||||
except exception.SnapshotNotFound:
|
except exception.SnapshotNotFound:
|
||||||
LOG.debug("Could not find temp snapshot %(snap)s to clean "
|
LOG.debug("Could not find temp snapshot %(snap)s to clean "
|
||||||
@ -441,12 +445,12 @@ class BackupManager(manager.ThreadPoolManager):
|
|||||||
if (isinstance(device_path, six.string_types) and
|
if (isinstance(device_path, six.string_types) and
|
||||||
not os.path.isdir(device_path)):
|
not os.path.isdir(device_path)):
|
||||||
if backup_device.secure_enabled:
|
if backup_device.secure_enabled:
|
||||||
with open(device_path) as device_file:
|
with open(device_path, 'rb') as device_file:
|
||||||
updates = backup_service.backup(
|
updates = backup_service.backup(
|
||||||
backup, tpool.Proxy(device_file))
|
backup, tpool.Proxy(device_file))
|
||||||
else:
|
else:
|
||||||
with utils.temporary_chown(device_path):
|
with utils.temporary_chown(device_path):
|
||||||
with open(device_path) as device_file:
|
with open(device_path, 'rb') as device_file:
|
||||||
updates = backup_service.backup(
|
updates = backup_service.backup(
|
||||||
backup, tpool.Proxy(device_file))
|
backup, tpool.Proxy(device_file))
|
||||||
# device_path is already file-like so no need to open it
|
# device_path is already file-like so no need to open it
|
||||||
@ -561,15 +565,16 @@ class BackupManager(manager.ThreadPoolManager):
|
|||||||
# with native threads proxy-wrapping the device file object.
|
# with native threads proxy-wrapping the device file object.
|
||||||
try:
|
try:
|
||||||
device_path = attach_info['device']['path']
|
device_path = attach_info['device']['path']
|
||||||
|
open_mode = 'rb+' if os.name == 'nt' else 'wb'
|
||||||
if (isinstance(device_path, six.string_types) and
|
if (isinstance(device_path, six.string_types) and
|
||||||
not os.path.isdir(device_path)):
|
not os.path.isdir(device_path)):
|
||||||
if secure_enabled:
|
if secure_enabled:
|
||||||
with open(device_path, 'wb') as device_file:
|
with open(device_path, open_mode) as device_file:
|
||||||
backup_service.restore(backup, volume.id,
|
backup_service.restore(backup, volume.id,
|
||||||
tpool.Proxy(device_file))
|
tpool.Proxy(device_file))
|
||||||
else:
|
else:
|
||||||
with utils.temporary_chown(device_path):
|
with utils.temporary_chown(device_path):
|
||||||
with open(device_path, 'wb') as device_file:
|
with open(device_path, open_mode) as device_file:
|
||||||
backup_service.restore(backup, volume.id,
|
backup_service.restore(backup, volume.id,
|
||||||
tpool.Proxy(device_file))
|
tpool.Proxy(device_file))
|
||||||
# device_path is already file-like so no need to open it
|
# device_path is already file-like so no need to open it
|
||||||
@ -1046,7 +1051,8 @@ class BackupManager(manager.ThreadPoolManager):
|
|||||||
protocol,
|
protocol,
|
||||||
use_multipath=use_multipath,
|
use_multipath=use_multipath,
|
||||||
device_scan_attempts=device_scan_attempts,
|
device_scan_attempts=device_scan_attempts,
|
||||||
conn=conn)
|
conn=conn,
|
||||||
|
expect_raw_disk=True)
|
||||||
vol_handle = connector.connect_volume(conn['data'])
|
vol_handle = connector.connect_volume(conn['data'])
|
||||||
|
|
||||||
return {'conn': conn, 'device': vol_handle, 'connector': connector}
|
return {'conn': conn, 'device': vol_handle, 'connector': connector}
|
||||||
|
@ -32,6 +32,7 @@ import mock
|
|||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from swiftclient import client as swift
|
from swiftclient import client as swift
|
||||||
|
|
||||||
|
from cinder.backup import chunkeddriver
|
||||||
from cinder.backup.drivers import swift as swift_dr
|
from cinder.backup.drivers import swift as swift_dr
|
||||||
from cinder import context
|
from cinder import context
|
||||||
from cinder import db
|
from cinder import db
|
||||||
@ -873,3 +874,71 @@ class BackupSwiftTestCase(test.TestCase):
|
|||||||
|
|
||||||
self.assertEqual('none', result[0])
|
self.assertEqual('none', result[0])
|
||||||
self.assertEqual(already_compressed_data, result[1])
|
self.assertEqual(already_compressed_data, result[1])
|
||||||
|
|
||||||
|
|
||||||
|
class WindowsBackupSwiftTestCase(BackupSwiftTestCase):
|
||||||
|
# We're running all the parent class tests, while doing
|
||||||
|
# some patching in order to simulate Windows behavior.
|
||||||
|
def setUp(self):
|
||||||
|
self._mock_utilsfactory = mock.Mock()
|
||||||
|
|
||||||
|
platform_patcher = mock.patch('sys.platform', 'win32')
|
||||||
|
platform_patcher.start()
|
||||||
|
self.addCleanup(platform_patcher.stop)
|
||||||
|
|
||||||
|
super(WindowsBackupSwiftTestCase, self).setUp()
|
||||||
|
|
||||||
|
read = self.volume_file.read
|
||||||
|
|
||||||
|
def win32_read(sz):
|
||||||
|
# We're simulating the Windows behavior.
|
||||||
|
if self.volume_file.tell() > fake_get_size():
|
||||||
|
raise IOError()
|
||||||
|
return read(sz)
|
||||||
|
|
||||||
|
read_patcher = mock.patch.object(
|
||||||
|
self.volume_file, 'read', win32_read)
|
||||||
|
read_patcher.start()
|
||||||
|
self.addCleanup(read_patcher.stop)
|
||||||
|
|
||||||
|
def fake_get_size(*args, **kwargs):
|
||||||
|
pos = self.volume_file.tell()
|
||||||
|
sz = self.volume_file.seek(0, 2)
|
||||||
|
self.volume_file.seek(pos)
|
||||||
|
return sz
|
||||||
|
|
||||||
|
self._disk_size_getter_mocker = mock.patch.object(
|
||||||
|
swift_dr.SwiftBackupDriver,
|
||||||
|
'_get_win32_phys_disk_size',
|
||||||
|
fake_get_size)
|
||||||
|
|
||||||
|
self._disk_size_getter_mocker.start()
|
||||||
|
self.addCleanup(self._disk_size_getter_mocker.stop)
|
||||||
|
|
||||||
|
def test_invalid_chunk_size(self):
|
||||||
|
self.flags(backup_swift_object_size=1000)
|
||||||
|
# We expect multiples of 4096
|
||||||
|
self.assertRaises(exception.InvalidConfigurationValue,
|
||||||
|
swift_dr.SwiftBackupDriver,
|
||||||
|
self.ctxt)
|
||||||
|
|
||||||
|
@mock.patch.object(chunkeddriver, 'os_win_utilsfactory', create=True)
|
||||||
|
def test_get_phys_disk_size(self, mock_utilsfactory):
|
||||||
|
# We're patching this method in setUp, so we need to
|
||||||
|
# retrieve the original one. Note that we'll get an unbound
|
||||||
|
# method.
|
||||||
|
service = swift_dr.SwiftBackupDriver(self.ctxt)
|
||||||
|
get_disk_size = self._disk_size_getter_mocker.temp_original
|
||||||
|
|
||||||
|
disk_utils = mock_utilsfactory.get_diskutils.return_value
|
||||||
|
disk_utils.get_device_number_from_device_name.return_value = (
|
||||||
|
mock.sentinel.dev_num)
|
||||||
|
disk_utils.get_disk_size.return_value = mock.sentinel.disk_size
|
||||||
|
|
||||||
|
disk_size = get_disk_size(service, mock.sentinel.disk_path)
|
||||||
|
|
||||||
|
self.assertEqual(mock.sentinel.disk_size, disk_size)
|
||||||
|
disk_utils.get_device_number_from_device_name.assert_called_once_with(
|
||||||
|
mock.sentinel.disk_path)
|
||||||
|
disk_utils.get_disk_size.assert_called_once_with(
|
||||||
|
mock.sentinel.dev_num)
|
||||||
|
@ -611,7 +611,7 @@ class BackupTestCase(BaseBackupTest):
|
|||||||
@mock.patch('cinder.utils.brick_get_connector_properties')
|
@mock.patch('cinder.utils.brick_get_connector_properties')
|
||||||
@mock.patch('cinder.volume.rpcapi.VolumeAPI.get_backup_device')
|
@mock.patch('cinder.volume.rpcapi.VolumeAPI.get_backup_device')
|
||||||
@mock.patch('cinder.utils.temporary_chown')
|
@mock.patch('cinder.utils.temporary_chown')
|
||||||
@mock.patch('six.moves.builtins.open')
|
@mock.patch('six.moves.builtins.open', wraps=open)
|
||||||
@mock.patch.object(os.path, 'isdir', return_value=False)
|
@mock.patch.object(os.path, 'isdir', return_value=False)
|
||||||
def test_create_backup(self, mock_isdir, mock_open, mock_temporary_chown,
|
def test_create_backup(self, mock_isdir, mock_open, mock_temporary_chown,
|
||||||
mock_get_backup_device, mock_get_conn):
|
mock_get_backup_device, mock_get_conn):
|
||||||
@ -636,7 +636,6 @@ class BackupTestCase(BaseBackupTest):
|
|||||||
mock_attach_device.return_value = attach_info
|
mock_attach_device.return_value = attach_info
|
||||||
properties = {}
|
properties = {}
|
||||||
mock_get_conn.return_value = properties
|
mock_get_conn.return_value = properties
|
||||||
mock_open.return_value = open('/dev/null', 'rb')
|
|
||||||
|
|
||||||
self.backup_mgr.create_backup(self.ctxt, backup)
|
self.backup_mgr.create_backup(self.ctxt, backup)
|
||||||
|
|
||||||
@ -650,6 +649,7 @@ class BackupTestCase(BaseBackupTest):
|
|||||||
force=True,
|
force=True,
|
||||||
ignore_errors=True)
|
ignore_errors=True)
|
||||||
|
|
||||||
|
mock_open.assert_called_once_with('/dev/null', 'rb')
|
||||||
vol = objects.Volume.get_by_id(self.ctxt, vol_id)
|
vol = objects.Volume.get_by_id(self.ctxt, vol_id)
|
||||||
self.assertEqual('available', vol['status'])
|
self.assertEqual('available', vol['status'])
|
||||||
self.assertEqual('backing-up', vol['previous_status'])
|
self.assertEqual('backing-up', vol['previous_status'])
|
||||||
@ -1084,10 +1084,14 @@ class BackupTestCase(BaseBackupTest):
|
|||||||
|
|
||||||
@mock.patch('cinder.utils.brick_get_connector_properties')
|
@mock.patch('cinder.utils.brick_get_connector_properties')
|
||||||
@mock.patch('cinder.utils.temporary_chown')
|
@mock.patch('cinder.utils.temporary_chown')
|
||||||
@mock.patch('six.moves.builtins.open')
|
@mock.patch('six.moves.builtins.open', wraps=open)
|
||||||
@mock.patch.object(os.path, 'isdir', return_value=False)
|
@mock.patch.object(os.path, 'isdir', return_value=False)
|
||||||
|
@ddt.data({'os_name': 'nt', 'exp_open_mode': 'rb+'},
|
||||||
|
{'os_name': 'posix', 'exp_open_mode': 'wb'})
|
||||||
|
@ddt.unpack
|
||||||
def test_restore_backup(self, mock_isdir, mock_open,
|
def test_restore_backup(self, mock_isdir, mock_open,
|
||||||
mock_temporary_chown, mock_get_conn):
|
mock_temporary_chown, mock_get_conn,
|
||||||
|
os_name, exp_open_mode):
|
||||||
"""Test normal backup restoration."""
|
"""Test normal backup restoration."""
|
||||||
vol_size = 1
|
vol_size = 1
|
||||||
vol_id = self._create_volume_db_entry(status='restoring-backup',
|
vol_id = self._create_volume_db_entry(status='restoring-backup',
|
||||||
@ -1097,7 +1101,6 @@ class BackupTestCase(BaseBackupTest):
|
|||||||
|
|
||||||
properties = {}
|
properties = {}
|
||||||
mock_get_conn.return_value = properties
|
mock_get_conn.return_value = properties
|
||||||
mock_open.return_value = open('/dev/null', 'wb')
|
|
||||||
mock_secure_enabled = (
|
mock_secure_enabled = (
|
||||||
self.volume_mocks['secure_file_operations_enabled'])
|
self.volume_mocks['secure_file_operations_enabled'])
|
||||||
mock_secure_enabled.return_value = False
|
mock_secure_enabled.return_value = False
|
||||||
@ -1109,8 +1112,10 @@ class BackupTestCase(BaseBackupTest):
|
|||||||
'_attach_device')
|
'_attach_device')
|
||||||
mock_attach_device.return_value = attach_info
|
mock_attach_device.return_value = attach_info
|
||||||
|
|
||||||
|
with mock.patch('os.name', os_name):
|
||||||
self.backup_mgr.restore_backup(self.ctxt, backup, vol_id)
|
self.backup_mgr.restore_backup(self.ctxt, backup, vol_id)
|
||||||
|
|
||||||
|
mock_open.assert_called_once_with('/dev/null', exp_open_mode)
|
||||||
mock_temporary_chown.assert_called_once_with('/dev/null')
|
mock_temporary_chown.assert_called_once_with('/dev/null')
|
||||||
mock_get_conn.assert_called_once_with()
|
mock_get_conn.assert_called_once_with()
|
||||||
mock_secure_enabled.assert_called_once_with(self.ctxt, vol)
|
mock_secure_enabled.assert_called_once_with(self.ctxt, vol)
|
||||||
|
@ -355,6 +355,16 @@ class TemporaryChownTestCase(test.TestCase):
|
|||||||
mock_stat.assert_called_once_with(test_filename)
|
mock_stat.assert_called_once_with(test_filename)
|
||||||
self.assertFalse(mock_exec.called)
|
self.assertFalse(mock_exec.called)
|
||||||
|
|
||||||
|
@mock.patch('os.name', 'nt')
|
||||||
|
@mock.patch('os.stat')
|
||||||
|
@mock.patch('cinder.utils.execute')
|
||||||
|
def test_temporary_chown_win32(self, mock_exec, mock_stat):
|
||||||
|
with utils.temporary_chown(mock.sentinel.path):
|
||||||
|
pass
|
||||||
|
|
||||||
|
mock_exec.assert_not_called()
|
||||||
|
mock_stat.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
class TempdirTestCase(test.TestCase):
|
class TempdirTestCase(test.TestCase):
|
||||||
@mock.patch('tempfile.mkdtemp')
|
@mock.patch('tempfile.mkdtemp')
|
||||||
|
@ -437,6 +437,13 @@ def temporary_chown(path, owner_uid=None):
|
|||||||
|
|
||||||
:params owner_uid: UID of temporary owner (defaults to current user)
|
:params owner_uid: UID of temporary owner (defaults to current user)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
if os.name == 'nt':
|
||||||
|
LOG.debug("Skipping chown for %s as this operation is "
|
||||||
|
"not available on Windows.", path)
|
||||||
|
yield
|
||||||
|
return
|
||||||
|
|
||||||
if owner_uid is None:
|
if owner_uid is None:
|
||||||
owner_uid = os.getuid()
|
owner_uid = os.getuid()
|
||||||
|
|
||||||
|
@ -1258,6 +1258,7 @@ class BaseVD(object):
|
|||||||
temp_snap_ref.destroy()
|
temp_snap_ref.destroy()
|
||||||
|
|
||||||
temp_snap_ref.status = fields.SnapshotStatus.AVAILABLE
|
temp_snap_ref.status = fields.SnapshotStatus.AVAILABLE
|
||||||
|
temp_snap_ref.progress = '100%'
|
||||||
temp_snap_ref.save()
|
temp_snap_ref.save()
|
||||||
return temp_snap_ref
|
return temp_snap_ref
|
||||||
|
|
||||||
|
@ -345,3 +345,6 @@ class WindowsISCSIDriver(driver.ISCSIDriver):
|
|||||||
additional_size_mb = (new_size - old_size) * 1024
|
additional_size_mb = (new_size - old_size) * 1024
|
||||||
|
|
||||||
self._tgt_utils.extend_wt_disk(volume.name, additional_size_mb)
|
self._tgt_utils.extend_wt_disk(volume.name, additional_size_mb)
|
||||||
|
|
||||||
|
def backup_use_temp_snapshot(self):
|
||||||
|
return False
|
||||||
|
@ -655,3 +655,6 @@ class WindowsSmbfsDriver(remotefs_drv.RevertToSnapshotMixin,
|
|||||||
# The SMBFS driver does not manage file permissions. We chose
|
# The SMBFS driver does not manage file permissions. We chose
|
||||||
# to let this up to the deployer.
|
# to let this up to the deployer.
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def backup_use_temp_snapshot(self):
|
||||||
|
return True
|
||||||
|
@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
The Cinder Volume Backup service can now be run on Windows. It supports
|
||||||
|
backing up volumes exposed by SMBFS/iSCSI Windows Cinder Volume backends,
|
||||||
|
as well as any other Cinder backend that's accessible on Windows (e.g.
|
||||||
|
SANs exposing volumes via iSCSI/FC).
|
||||||
|
|
||||||
|
The Swift and Posix backup drivers are known to be working on Windows.
|
Loading…
Reference in New Issue
Block a user