Support Incremental Backup Completion In RBD
Ceph RBD backend ignores the `--incremental` option when creating a volume backup. The first backup of a given volume is always a full backup, and each subsequent backup is always an incremental backup. This behavior makes it impossible to remove old backups while keeping at least one recent backup. Since Cinder will not find the latest_backup id as parent_id if '--incremental=False', so we can use the parent_id to ensure whether do the full backup in rbd driver or not. If the incremental flag '--incremental' is not specified, this patch will always create a new full backup for rbd volume. Change-Id: I516b7c82b05b26e81195f7f106d43a9e0804082d Closes-Bug: #1810270 Closes-Bug: #1790713 Co-Authored-By: Sofia Enriquez <lsofia.enriquez@gmail.com>
This commit is contained in:
parent
45dc00518c
commit
5018727f8e
@ -278,7 +278,10 @@ class API(base.Base):
|
|||||||
raise exception.InvalidBackup(reason=msg)
|
raise exception.InvalidBackup(reason=msg)
|
||||||
|
|
||||||
parent_id = None
|
parent_id = None
|
||||||
|
parent = None
|
||||||
|
|
||||||
if latest_backup:
|
if latest_backup:
|
||||||
|
parent = latest_backup
|
||||||
parent_id = latest_backup.id
|
parent_id = latest_backup.id
|
||||||
if latest_backup['status'] != fields.BackupStatus.AVAILABLE:
|
if latest_backup['status'] != fields.BackupStatus.AVAILABLE:
|
||||||
msg = _('The parent backup must be available for '
|
msg = _('The parent backup must be available for '
|
||||||
@ -313,6 +316,7 @@ class API(base.Base):
|
|||||||
'availability_zone': availability_zone,
|
'availability_zone': availability_zone,
|
||||||
'snapshot_id': snapshot_id,
|
'snapshot_id': snapshot_id,
|
||||||
'data_timestamp': data_timestamp,
|
'data_timestamp': data_timestamp,
|
||||||
|
'parent': parent,
|
||||||
'metadata': metadata or {}
|
'metadata': metadata or {}
|
||||||
}
|
}
|
||||||
backup = objects.Backup(context=context, **kwargs)
|
backup = objects.Backup(context=context, **kwargs)
|
||||||
|
@ -43,6 +43,7 @@ restore to a new volume (default).
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
import fcntl
|
import fcntl
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
@ -314,22 +315,39 @@ class CephBackupDriver(driver.BackupDriver):
|
|||||||
ioctx.close()
|
ioctx.close()
|
||||||
client.shutdown()
|
client.shutdown()
|
||||||
|
|
||||||
def _get_backup_base_name(self, volume_id, backup_id=None,
|
def _format_base_name(self, service_metadata):
|
||||||
diff_format=False):
|
base_name = json.loads(service_metadata)["base"]
|
||||||
|
return utils.convert_str(base_name)
|
||||||
|
|
||||||
|
def _get_backup_base_name(self, volume_id, backup=None):
|
||||||
"""Return name of base image used for backup.
|
"""Return name of base image used for backup.
|
||||||
|
|
||||||
Incremental backups use a new base name so we support old and new style
|
Incremental backups use a new base name so we support old and new style
|
||||||
format.
|
format.
|
||||||
"""
|
"""
|
||||||
# Ensure no unicode
|
# Ensure no unicode
|
||||||
if diff_format:
|
if not backup:
|
||||||
return utils.convert_str("volume-%s.backup.base" % volume_id)
|
return utils.convert_str("volume-%s.backup.base" % volume_id)
|
||||||
else:
|
|
||||||
if backup_id is None:
|
if backup.service_metadata:
|
||||||
msg = _("Backup id required")
|
return self._format_base_name(backup.service_metadata)
|
||||||
raise exception.InvalidParameterValue(msg)
|
|
||||||
return utils.convert_str("volume-%s.backup.%s"
|
# 'parent' field will only be present in incremental backups. This is
|
||||||
% (volume_id, backup_id))
|
# filled by cinder-api
|
||||||
|
if backup.parent:
|
||||||
|
# Old backups don't have the base name in the service_metadata,
|
||||||
|
# so we use the default RBD backup base
|
||||||
|
if backup.parent.service_metadata:
|
||||||
|
service_metadata = backup.parent.service_metadata
|
||||||
|
base_name = self._format_base_name(service_metadata)
|
||||||
|
else:
|
||||||
|
base_name = utils.convert_str("volume-%s.backup.base"
|
||||||
|
% volume_id)
|
||||||
|
|
||||||
|
return base_name
|
||||||
|
|
||||||
|
return utils.convert_str("volume-%s.backup.%s"
|
||||||
|
% (volume_id, backup.id))
|
||||||
|
|
||||||
def _discard_bytes(self, volume, offset, length):
|
def _discard_bytes(self, volume, offset, length):
|
||||||
"""Trim length bytes from offset.
|
"""Trim length bytes from offset.
|
||||||
@ -479,7 +497,7 @@ class CephBackupDriver(driver.BackupDriver):
|
|||||||
if base_name is None:
|
if base_name is None:
|
||||||
try_diff_format = True
|
try_diff_format = True
|
||||||
|
|
||||||
base_name = self._get_backup_base_name(volume_id, backup.id)
|
base_name = self._get_backup_base_name(volume_id, backup=backup)
|
||||||
LOG.debug("Trying diff format basename='%(basename)s' for "
|
LOG.debug("Trying diff format basename='%(basename)s' for "
|
||||||
"backup base image of volume %(volume)s.",
|
"backup base image of volume %(volume)s.",
|
||||||
{'basename': base_name, 'volume': volume_id})
|
{'basename': base_name, 'volume': volume_id})
|
||||||
@ -630,7 +648,7 @@ class CephBackupDriver(driver.BackupDriver):
|
|||||||
if name not in rbds:
|
if name not in rbds:
|
||||||
LOG.debug("Image '%s' not found - trying diff format name", name)
|
LOG.debug("Image '%s' not found - trying diff format name", name)
|
||||||
if try_diff_format:
|
if try_diff_format:
|
||||||
name = self._get_backup_base_name(volume_id, diff_format=True)
|
name = self._get_backup_base_name(volume_id)
|
||||||
if name not in rbds:
|
if name not in rbds:
|
||||||
LOG.debug("Diff format image '%s' not found", name)
|
LOG.debug("Diff format image '%s' not found", name)
|
||||||
return False, name
|
return False, name
|
||||||
@ -657,50 +675,79 @@ class CephBackupDriver(driver.BackupDriver):
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def _full_rbd_backup(self, container, base_name, length):
|
||||||
|
"""Create the base_image for a full RBD backup."""
|
||||||
|
with eventlet.tpool.Proxy(rbd_driver.RADOSClient(self,
|
||||||
|
container)) as client:
|
||||||
|
self._create_base_image(base_name, length, client)
|
||||||
|
# Now we just need to return from_snap=None and image_created=True, if
|
||||||
|
# there is some exception in making backup snapshot, will clean up the
|
||||||
|
# base image.
|
||||||
|
return None, True
|
||||||
|
|
||||||
|
def _incremental_rbd_backup(self, backup, base_name, length,
|
||||||
|
source_rbd_image, volume_id):
|
||||||
|
"""Select the last snapshot for a RBD incremental backup."""
|
||||||
|
|
||||||
|
container = backup.container
|
||||||
|
last_incr = backup.parent_id
|
||||||
|
LOG.debug("Trying to perform an incremental backup with container: "
|
||||||
|
"%(container)s, base_name: %(base)s, source RBD image: "
|
||||||
|
"%(source)s, volume ID %(volume)s and last incremental "
|
||||||
|
"backup ID: %(incr)s.",
|
||||||
|
{'container': container,
|
||||||
|
'base': base_name,
|
||||||
|
'source': source_rbd_image,
|
||||||
|
'volume': volume_id,
|
||||||
|
'incr': last_incr,
|
||||||
|
})
|
||||||
|
|
||||||
|
with eventlet.tpool.Proxy(rbd_driver.RADOSClient(self,
|
||||||
|
container)) as client:
|
||||||
|
base_rbd = eventlet.tpool.Proxy(self.rbd.Image(client.ioctx,
|
||||||
|
base_name,
|
||||||
|
read_only=True))
|
||||||
|
try:
|
||||||
|
from_snap = self._get_backup_snap_name(base_rbd,
|
||||||
|
base_name,
|
||||||
|
last_incr)
|
||||||
|
if from_snap is None:
|
||||||
|
msg = (_(
|
||||||
|
"Can't find snapshot from parent %(incr)s and "
|
||||||
|
"base name image %(base)s.") %
|
||||||
|
{'incr': last_incr, 'base': base_name})
|
||||||
|
LOG.error(msg)
|
||||||
|
raise exception.BackupRBDOperationFailed(msg)
|
||||||
|
finally:
|
||||||
|
base_rbd.close()
|
||||||
|
|
||||||
|
return from_snap, False
|
||||||
|
|
||||||
def _backup_rbd(self, backup, volume_file, volume_name, length):
|
def _backup_rbd(self, backup, volume_file, volume_name, length):
|
||||||
"""Create an incremental backup from an RBD image."""
|
"""Create an incremental or full backup from an RBD image."""
|
||||||
rbd_user = volume_file.rbd_user
|
rbd_user = volume_file.rbd_user
|
||||||
rbd_pool = volume_file.rbd_pool
|
rbd_pool = volume_file.rbd_pool
|
||||||
rbd_conf = volume_file.rbd_conf
|
rbd_conf = volume_file.rbd_conf
|
||||||
source_rbd_image = eventlet.tpool.Proxy(volume_file.rbd_image)
|
source_rbd_image = eventlet.tpool.Proxy(volume_file.rbd_image)
|
||||||
volume_id = backup.volume_id
|
volume_id = backup.volume_id
|
||||||
updates = {}
|
base_name = None
|
||||||
base_name = self._get_backup_base_name(volume_id, diff_format=True)
|
|
||||||
image_created = False
|
|
||||||
with eventlet.tpool.Proxy(rbd_driver.RADOSClient(self,
|
|
||||||
backup.container)) as client:
|
|
||||||
# If from_snap does not exist at the destination (and the
|
|
||||||
# destination exists), this implies a previous backup has failed.
|
|
||||||
# In this case we will force a full backup.
|
|
||||||
#
|
|
||||||
# TODO(dosaboy): find a way to repair the broken backup
|
|
||||||
#
|
|
||||||
if base_name not in eventlet.tpool.Proxy(self.rbd.RBD()).list(
|
|
||||||
ioctx=client.ioctx):
|
|
||||||
src_vol_snapshots = self.get_backup_snaps(source_rbd_image)
|
|
||||||
if src_vol_snapshots:
|
|
||||||
# If there are source volume snapshots but base does not
|
|
||||||
# exist then we delete it and set from_snap to None
|
|
||||||
LOG.debug("Volume '%(volume)s' has stale source "
|
|
||||||
"snapshots so deleting them.",
|
|
||||||
{'volume': volume_id})
|
|
||||||
for snap in src_vol_snapshots:
|
|
||||||
from_snap = snap['name']
|
|
||||||
source_rbd_image.remove_snap(from_snap)
|
|
||||||
from_snap = None
|
|
||||||
|
|
||||||
# Create new base image
|
# If backup.parent_id is None performs full RBD backup
|
||||||
self._create_base_image(base_name, length, client)
|
if backup.parent_id is None:
|
||||||
image_created = True
|
base_name = self._get_backup_base_name(volume_id, backup=backup)
|
||||||
else:
|
from_snap, image_created = self._full_rbd_backup(backup.container,
|
||||||
# If a from_snap is defined and is present in the source volume
|
base_name,
|
||||||
# image but does not exist in the backup base then we look down
|
length)
|
||||||
# the list of source volume snapshots and find the latest one
|
# Otherwise performs incremental rbd backup
|
||||||
# for which a backup snapshot exist in the backup base. Until
|
else:
|
||||||
# that snapshot is reached, we delete all the other snapshots
|
# Find the base name from the parent backup's service_metadata
|
||||||
# for which backup snapshot does not exist.
|
base_name = self._get_backup_base_name(volume_id, backup=backup)
|
||||||
from_snap = self._get_most_recent_snap(source_rbd_image,
|
rbd_img = source_rbd_image
|
||||||
base_name, client)
|
from_snap, image_created = self._incremental_rbd_backup(backup,
|
||||||
|
base_name,
|
||||||
|
length,
|
||||||
|
rbd_img,
|
||||||
|
volume_id)
|
||||||
|
|
||||||
LOG.debug("Using --from-snap '%(snap)s' for incremental backup of "
|
LOG.debug("Using --from-snap '%(snap)s' for incremental backup of "
|
||||||
"volume %(volume)s.",
|
"volume %(volume)s.",
|
||||||
@ -744,14 +791,8 @@ class CephBackupDriver(driver.BackupDriver):
|
|||||||
"source volume='%(volume)s'.",
|
"source volume='%(volume)s'.",
|
||||||
{'snapshot': new_snap, 'volume': volume_id})
|
{'snapshot': new_snap, 'volume': volume_id})
|
||||||
source_rbd_image.remove_snap(new_snap)
|
source_rbd_image.remove_snap(new_snap)
|
||||||
# We update the parent_id here. The from_snap is of the format:
|
|
||||||
# backup.BACKUP_ID.snap.TIMESTAMP. So we need to extract the
|
return {'service_metadata': '{"base": "%s"}' % base_name}
|
||||||
# backup_id of the parent only from from_snap and set it as
|
|
||||||
# parent_id
|
|
||||||
if from_snap:
|
|
||||||
parent_id = from_snap.split('.')
|
|
||||||
updates = {'parent_id': parent_id[1]}
|
|
||||||
return updates
|
|
||||||
|
|
||||||
def _file_is_rbd(self, volume_file):
|
def _file_is_rbd(self, volume_file):
|
||||||
"""Returns True if the volume_file is actually an RBD image."""
|
"""Returns True if the volume_file is actually an RBD image."""
|
||||||
@ -765,7 +806,7 @@ class CephBackupDriver(driver.BackupDriver):
|
|||||||
image.
|
image.
|
||||||
"""
|
"""
|
||||||
volume_id = backup.volume_id
|
volume_id = backup.volume_id
|
||||||
backup_name = self._get_backup_base_name(volume_id, backup.id)
|
backup_name = self._get_backup_base_name(volume_id, backup=backup)
|
||||||
|
|
||||||
with eventlet.tpool.Proxy(rbd_driver.RADOSClient(self,
|
with eventlet.tpool.Proxy(rbd_driver.RADOSClient(self,
|
||||||
backup.container)) as client:
|
backup.container)) as client:
|
||||||
@ -868,23 +909,6 @@ class CephBackupDriver(driver.BackupDriver):
|
|||||||
LOG.debug("Found snapshot '%s'", snaps[0])
|
LOG.debug("Found snapshot '%s'", snaps[0])
|
||||||
return snaps[0]
|
return snaps[0]
|
||||||
|
|
||||||
def _get_most_recent_snap(self, rbd_image, base_name, client):
|
|
||||||
"""Get the most recent backup snapshot of the provided image.
|
|
||||||
|
|
||||||
Returns name of most recent backup snapshot or None if there are no
|
|
||||||
backup snapshots.
|
|
||||||
"""
|
|
||||||
src_vol_backup_snaps = self.get_backup_snaps(rbd_image, sort=True)
|
|
||||||
from_snap = None
|
|
||||||
|
|
||||||
for snap in src_vol_backup_snaps:
|
|
||||||
if self._snap_exists(base_name, snap['name'], client):
|
|
||||||
from_snap = snap['name']
|
|
||||||
break
|
|
||||||
rbd_image.remove_snap(snap['name'])
|
|
||||||
|
|
||||||
return from_snap
|
|
||||||
|
|
||||||
def _get_volume_size_gb(self, volume):
|
def _get_volume_size_gb(self, volume):
|
||||||
"""Return the size in gigabytes of the given volume.
|
"""Return the size in gigabytes of the given volume.
|
||||||
|
|
||||||
@ -938,17 +962,23 @@ class CephBackupDriver(driver.BackupDriver):
|
|||||||
volume_file.seek(0)
|
volume_file.seek(0)
|
||||||
length = self._get_volume_size_gb(volume)
|
length = self._get_volume_size_gb(volume)
|
||||||
|
|
||||||
do_full_backup = False
|
if backup.snapshot_id:
|
||||||
if self._file_is_rbd(volume_file):
|
do_full_backup = True
|
||||||
# If volume an RBD, attempt incremental backup.
|
elif self._file_is_rbd(volume_file):
|
||||||
LOG.debug("Volume file is RBD: attempting incremental backup.")
|
# If volume an RBD, attempt incremental or full backup.
|
||||||
|
do_full_backup = False
|
||||||
|
LOG.debug("Volume file is RBD: attempting optimized backup")
|
||||||
try:
|
try:
|
||||||
updates = self._backup_rbd(backup, volume_file,
|
updates = self._backup_rbd(backup, volume_file, volume.name,
|
||||||
volume.name, length)
|
length)
|
||||||
except exception.BackupRBDOperationFailed:
|
except exception.BackupRBDOperationFailed:
|
||||||
LOG.debug("Forcing full backup of volume %s.", volume.id)
|
with excutils.save_and_reraise_exception():
|
||||||
do_full_backup = True
|
self.delete_backup(backup)
|
||||||
else:
|
else:
|
||||||
|
if backup.parent_id:
|
||||||
|
LOG.debug("Volume file is NOT RBD: can't perform"
|
||||||
|
"incremental backup.")
|
||||||
|
raise exception.BackupRBDOperationFailed
|
||||||
LOG.debug("Volume file is NOT RBD: will do full backup.")
|
LOG.debug("Volume file is NOT RBD: will do full backup.")
|
||||||
do_full_backup = True
|
do_full_backup = True
|
||||||
|
|
||||||
@ -970,11 +1000,6 @@ class CephBackupDriver(driver.BackupDriver):
|
|||||||
LOG.debug("Backup '%(backup_id)s' of volume %(volume_id)s finished.",
|
LOG.debug("Backup '%(backup_id)s' of volume %(volume_id)s finished.",
|
||||||
{'backup_id': backup.id, 'volume_id': volume.id})
|
{'backup_id': backup.id, 'volume_id': volume.id})
|
||||||
|
|
||||||
# If updates is empty then set parent_id to None. This will
|
|
||||||
# take care if --incremental flag is used in CLI but a full
|
|
||||||
# backup is performed instead
|
|
||||||
if not updates and backup.parent_id:
|
|
||||||
updates = {'parent_id': None}
|
|
||||||
return updates
|
return updates
|
||||||
|
|
||||||
def _full_restore(self, backup, dest_file, dest_name, length,
|
def _full_restore(self, backup, dest_file, dest_name, length,
|
||||||
@ -989,13 +1014,10 @@ class CephBackupDriver(driver.BackupDriver):
|
|||||||
# If a source snapshot is provided we assume the base is diff
|
# If a source snapshot is provided we assume the base is diff
|
||||||
# format.
|
# format.
|
||||||
if src_snap:
|
if src_snap:
|
||||||
diff_format = True
|
backup_name = self._get_backup_base_name(backup.volume_id,
|
||||||
|
backup=backup)
|
||||||
else:
|
else:
|
||||||
diff_format = False
|
backup_name = self._get_backup_base_name(backup.volume_id)
|
||||||
|
|
||||||
backup_name = self._get_backup_base_name(backup.volume_id,
|
|
||||||
backup_id=backup.id,
|
|
||||||
diff_format=diff_format)
|
|
||||||
|
|
||||||
# Retrieve backup volume
|
# Retrieve backup volume
|
||||||
src_rbd = eventlet.tpool.Proxy(self.rbd.Image(client.ioctx,
|
src_rbd = eventlet.tpool.Proxy(self.rbd.Image(client.ioctx,
|
||||||
@ -1022,7 +1044,7 @@ class CephBackupDriver(driver.BackupDriver):
|
|||||||
post-process and resize it back to its expected size.
|
post-process and resize it back to its expected size.
|
||||||
"""
|
"""
|
||||||
backup_base = self._get_backup_base_name(backup.volume_id,
|
backup_base = self._get_backup_base_name(backup.volume_id,
|
||||||
diff_format=True)
|
backup=backup)
|
||||||
|
|
||||||
with eventlet.tpool.Proxy(rbd_driver.RADOSClient(self,
|
with eventlet.tpool.Proxy(rbd_driver.RADOSClient(self,
|
||||||
backup.container)) as client:
|
backup.container)) as client:
|
||||||
@ -1047,7 +1069,7 @@ class CephBackupDriver(driver.BackupDriver):
|
|||||||
rbd_pool = restore_file.rbd_pool
|
rbd_pool = restore_file.rbd_pool
|
||||||
rbd_conf = restore_file.rbd_conf
|
rbd_conf = restore_file.rbd_conf
|
||||||
base_name = self._get_backup_base_name(backup.volume_id,
|
base_name = self._get_backup_base_name(backup.volume_id,
|
||||||
diff_format=True)
|
backup=backup)
|
||||||
|
|
||||||
LOG.debug("Attempting incremental restore from base='%(base)s' "
|
LOG.debug("Attempting incremental restore from base='%(base)s' "
|
||||||
"snap='%(snap)s'",
|
"snap='%(snap)s'",
|
||||||
@ -1179,8 +1201,10 @@ class CephBackupDriver(driver.BackupDriver):
|
|||||||
"""
|
"""
|
||||||
length = int(volume.size) * units.Gi
|
length = int(volume.size) * units.Gi
|
||||||
|
|
||||||
base_name = self._get_backup_base_name(backup.volume_id,
|
if backup.service_metadata:
|
||||||
diff_format=True)
|
base_name = self._get_backup_base_name(backup.volume_id, backup)
|
||||||
|
else:
|
||||||
|
base_name = self._get_backup_base_name(backup.volume_id)
|
||||||
|
|
||||||
with eventlet.tpool.Proxy(rbd_driver.RADOSClient(
|
with eventlet.tpool.Proxy(rbd_driver.RADOSClient(
|
||||||
self, backup.container)) as client:
|
self, backup.container)) as client:
|
||||||
|
@ -40,7 +40,8 @@ class Backup(base.CinderPersistentObject, base.CinderObject,
|
|||||||
# Version 1.4: Add restore_volume_id
|
# Version 1.4: Add restore_volume_id
|
||||||
# Version 1.5: Add metadata
|
# Version 1.5: Add metadata
|
||||||
# Version 1.6: Add encryption_key_id
|
# Version 1.6: Add encryption_key_id
|
||||||
VERSION = '1.6'
|
# Version 1.7: Add parent
|
||||||
|
VERSION = '1.7'
|
||||||
|
|
||||||
OPTIONAL_FIELDS = ('metadata',)
|
OPTIONAL_FIELDS = ('metadata',)
|
||||||
|
|
||||||
@ -55,6 +56,7 @@ class Backup(base.CinderPersistentObject, base.CinderObject,
|
|||||||
'availability_zone': fields.StringField(nullable=True),
|
'availability_zone': fields.StringField(nullable=True),
|
||||||
'container': fields.StringField(nullable=True),
|
'container': fields.StringField(nullable=True),
|
||||||
'parent_id': fields.StringField(nullable=True),
|
'parent_id': fields.StringField(nullable=True),
|
||||||
|
'parent': fields.ObjectField('Backup', nullable=True),
|
||||||
'status': c_fields.BackupStatusField(nullable=True),
|
'status': c_fields.BackupStatusField(nullable=True),
|
||||||
'fail_reason': fields.StringField(nullable=True),
|
'fail_reason': fields.StringField(nullable=True),
|
||||||
'size': fields.IntegerField(nullable=True),
|
'size': fields.IntegerField(nullable=True),
|
||||||
@ -110,8 +112,14 @@ class Backup(base.CinderPersistentObject, base.CinderObject,
|
|||||||
|
|
||||||
def obj_make_compatible(self, primitive, target_version):
|
def obj_make_compatible(self, primitive, target_version):
|
||||||
"""Make an object representation compatible with a target version."""
|
"""Make an object representation compatible with a target version."""
|
||||||
|
added_fields = (((1, 7), ('parent',)),)
|
||||||
|
|
||||||
super(Backup, self).obj_make_compatible(primitive, target_version)
|
super(Backup, self).obj_make_compatible(primitive, target_version)
|
||||||
target_version = versionutils.convert_version_to_tuple(target_version)
|
target_version = versionutils.convert_version_to_tuple(target_version)
|
||||||
|
for version, remove_fields in added_fields:
|
||||||
|
if target_version < version:
|
||||||
|
for obj_field in remove_fields:
|
||||||
|
primitive.pop(obj_field, None)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _from_db_object(cls, context, backup, db_backup, expected_attrs=None):
|
def _from_db_object(cls, context, backup, db_backup, expected_attrs=None):
|
||||||
@ -174,6 +182,7 @@ class Backup(base.CinderPersistentObject, base.CinderObject,
|
|||||||
self.metadata = db.backup_metadata_update(self._context,
|
self.metadata = db.backup_metadata_update(self._context,
|
||||||
self.id, metadata,
|
self.id, metadata,
|
||||||
True)
|
True)
|
||||||
|
updates.pop('parent', None)
|
||||||
db.backup_update(self._context, self.id, updates)
|
db.backup_update(self._context, self.id, updates)
|
||||||
|
|
||||||
self.obj_reset_changes()
|
self.obj_reset_changes()
|
||||||
|
@ -150,6 +150,7 @@ OBJ_VERSIONS.add('1.34', {'VolumeAttachment': '1.3'})
|
|||||||
OBJ_VERSIONS.add('1.35', {'Backup': '1.6', 'BackupImport': '1.6'})
|
OBJ_VERSIONS.add('1.35', {'Backup': '1.6', 'BackupImport': '1.6'})
|
||||||
OBJ_VERSIONS.add('1.36', {'RequestSpec': '1.4'})
|
OBJ_VERSIONS.add('1.36', {'RequestSpec': '1.4'})
|
||||||
OBJ_VERSIONS.add('1.37', {'RequestSpec': '1.5'})
|
OBJ_VERSIONS.add('1.37', {'RequestSpec': '1.5'})
|
||||||
|
OBJ_VERSIONS.add('1.38', {'Backup': '1.7', 'BackupImport': '1.7'})
|
||||||
|
|
||||||
|
|
||||||
class CinderObjectRegistry(base.VersionedObjectRegistry):
|
class CinderObjectRegistry(base.VersionedObjectRegistry):
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
""" Tests for Ceph backup service."""
|
""" Tests for Ceph backup service."""
|
||||||
|
|
||||||
import hashlib
|
import hashlib
|
||||||
|
import json
|
||||||
import os
|
import os
|
||||||
import tempfile
|
import tempfile
|
||||||
import threading
|
import threading
|
||||||
@ -39,6 +40,7 @@ from cinder.i18n import _
|
|||||||
from cinder import objects
|
from cinder import objects
|
||||||
from cinder import test
|
from cinder import test
|
||||||
from cinder.tests.unit import fake_constants as fake
|
from cinder.tests.unit import fake_constants as fake
|
||||||
|
import cinder.volume.drivers.rbd as rbd_driver
|
||||||
|
|
||||||
# This is used to collect raised exceptions so that tests may check what was
|
# This is used to collect raised exceptions so that tests may check what was
|
||||||
# raised.
|
# raised.
|
||||||
@ -119,6 +121,14 @@ class BackupCephTestCase(test.TestCase):
|
|||||||
'user_id': userid, 'project_id': projectid}
|
'user_id': userid, 'project_id': projectid}
|
||||||
return db.backup_create(self.ctxt, backup)['id']
|
return db.backup_create(self.ctxt, backup)['id']
|
||||||
|
|
||||||
|
def _create_parent_backup_object(self):
|
||||||
|
tmp_backup_id = fake.BACKUP3_ID
|
||||||
|
self._create_backup_db_entry(tmp_backup_id, self.volume_id,
|
||||||
|
self.volume_size)
|
||||||
|
tmp_backup = objects.Backup.get_by_id(self.ctxt, tmp_backup_id)
|
||||||
|
tmp_backup.service_metadata = 'mock_base_name'
|
||||||
|
return tmp_backup
|
||||||
|
|
||||||
def time_inc(self):
|
def time_inc(self):
|
||||||
self.counter += 1
|
self.counter += 1
|
||||||
return self.counter
|
return self.counter
|
||||||
@ -170,6 +180,22 @@ class BackupCephTestCase(test.TestCase):
|
|||||||
self.backup = objects.Backup.get_by_id(self.ctxt, self.backup_id)
|
self.backup = objects.Backup.get_by_id(self.ctxt, self.backup_id)
|
||||||
self.backup.container = "backups"
|
self.backup.container = "backups"
|
||||||
|
|
||||||
|
# Create parent backup of volume
|
||||||
|
self.parent_backup = self._create_parent_backup_object()
|
||||||
|
|
||||||
|
# Create alternate backup with parent
|
||||||
|
self.alt_backup_id = fake.BACKUP2_ID
|
||||||
|
self._create_backup_db_entry(self.alt_backup_id, self.volume_id,
|
||||||
|
self.volume_size)
|
||||||
|
|
||||||
|
self.alt_backup = objects.Backup.get_by_id(self.ctxt,
|
||||||
|
self.alt_backup_id)
|
||||||
|
|
||||||
|
base_name = "volume-%s.backup.%s" % (self.volume_id, self.backup_id)
|
||||||
|
self.alt_backup.container = "backups"
|
||||||
|
self.alt_backup.parent = self.backup
|
||||||
|
self.alt_backup.parent.service_metadata = '{"base": "%s"}' % base_name
|
||||||
|
|
||||||
# Create alternate volume.
|
# Create alternate volume.
|
||||||
self.alt_volume_id = str(uuid.uuid4())
|
self.alt_volume_id = str(uuid.uuid4())
|
||||||
self._create_volume_db_entry(self.alt_volume_id, self.volume_size)
|
self._create_volume_db_entry(self.alt_volume_id, self.volume_size)
|
||||||
@ -255,24 +281,6 @@ class BackupCephTestCase(test.TestCase):
|
|||||||
self.assertFalse(oldformat)
|
self.assertFalse(oldformat)
|
||||||
self.assertEqual(1 | 2 | 4 | 64, features)
|
self.assertEqual(1 | 2 | 4 | 64, features)
|
||||||
|
|
||||||
@common_mocks
|
|
||||||
def test_get_most_recent_snap(self):
|
|
||||||
last = 'backup.%s.snap.9824923.1212' % (uuid.uuid4())
|
|
||||||
|
|
||||||
image = self.mock_rbd.Image.return_value
|
|
||||||
with mock.patch.object(self.service, '_snap_exists') as \
|
|
||||||
mock_snap_exists:
|
|
||||||
mock_snap_exists.return_value = True
|
|
||||||
image.list_snaps.return_value = \
|
|
||||||
[{'name': 'backup.%s.snap.6423868.2342' % (uuid.uuid4())},
|
|
||||||
{'name': 'backup.%s.snap.1321319.3235' % (uuid.uuid4())},
|
|
||||||
{'name': last},
|
|
||||||
{'name': 'backup.%s.snap.3824923.1412' % (uuid.uuid4())}]
|
|
||||||
base_name = "mock_base"
|
|
||||||
client = mock.Mock()
|
|
||||||
snap = self.service._get_most_recent_snap(image, base_name, client)
|
|
||||||
self.assertEqual(last, snap)
|
|
||||||
|
|
||||||
@common_mocks
|
@common_mocks
|
||||||
def test_get_backup_snap_name(self):
|
def test_get_backup_snap_name(self):
|
||||||
snap_name = 'backup.%s.snap.3824923.1412' % (uuid.uuid4())
|
snap_name = 'backup.%s.snap.3824923.1412' % (uuid.uuid4())
|
||||||
@ -415,7 +423,7 @@ class BackupCephTestCase(test.TestCase):
|
|||||||
with mock.patch.object(self.service, '_backup_metadata'):
|
with mock.patch.object(self.service, '_backup_metadata'):
|
||||||
with mock.patch.object(self.service, '_discard_bytes'):
|
with mock.patch.object(self.service, '_discard_bytes'):
|
||||||
with tempfile.NamedTemporaryFile() as test_file:
|
with tempfile.NamedTemporaryFile() as test_file:
|
||||||
self.service.backup(self.backup, self.volume_file)
|
self.service.backup(self.alt_backup, self.volume_file)
|
||||||
|
|
||||||
# Ensure the files are equal
|
# Ensure the files are equal
|
||||||
self.assertEqual(checksum.digest(), self.checksum.digest())
|
self.assertEqual(checksum.digest(), self.checksum.digest())
|
||||||
@ -424,25 +432,34 @@ class BackupCephTestCase(test.TestCase):
|
|||||||
self.assertNotEqual(threading.current_thread(), thread_dict['thread'])
|
self.assertNotEqual(threading.current_thread(), thread_dict['thread'])
|
||||||
|
|
||||||
@common_mocks
|
@common_mocks
|
||||||
def test_get_backup_base_name(self):
|
def test_get_backup_base_name_without_backup_param(self):
|
||||||
name = self.service._get_backup_base_name(self.volume_id,
|
"""Test _get_backup_base_name without backup."""
|
||||||
diff_format=True)
|
name = self.service._get_backup_base_name(self.volume_id)
|
||||||
self.assertEqual("volume-%s.backup.base" % (self.volume_id), name)
|
self.assertEqual("volume-%s.backup.base" % (self.volume_id), name)
|
||||||
|
|
||||||
self.assertRaises(exception.InvalidParameterValue,
|
@common_mocks
|
||||||
self.service._get_backup_base_name,
|
def test_get_backup_base_name_w_backup_and_no_parent(self):
|
||||||
self.volume_id)
|
"""Test _get_backup_base_name with backup and no parent."""
|
||||||
|
name = self.service._get_backup_base_name(self.volume_id,
|
||||||
|
self.backup)
|
||||||
|
self.assertEqual("volume-%s.backup.%s" %
|
||||||
|
(self.volume_id, self.backup.id), name)
|
||||||
|
|
||||||
name = self.service._get_backup_base_name(self.volume_id, '1234')
|
@common_mocks
|
||||||
self.assertEqual("volume-%s.backup.%s" % (self.volume_id, '1234'),
|
def test_get_backup_base_name_w_backup_and_parent(self):
|
||||||
name)
|
"""Test _get_backup_base_name with backup and parent."""
|
||||||
|
name = self.service._get_backup_base_name(self.volume_id,
|
||||||
|
self.alt_backup)
|
||||||
|
base_name = json.loads(self.alt_backup.parent.service_metadata)
|
||||||
|
self.assertEqual(base_name["base"], name)
|
||||||
|
|
||||||
@common_mocks
|
@common_mocks
|
||||||
@mock.patch('fcntl.fcntl', spec=True)
|
@mock.patch('fcntl.fcntl', spec=True)
|
||||||
@mock.patch('subprocess.Popen', spec=True)
|
@mock.patch('subprocess.Popen', spec=True)
|
||||||
def test_backup_volume_from_rbd(self, mock_popen, mock_fnctl):
|
def test_backup_volume_from_rbd(self, mock_popen, mock_fnctl):
|
||||||
|
"""Test full RBD backup generated successfully."""
|
||||||
backup_name = self.service._get_backup_base_name(self.volume_id,
|
backup_name = self.service._get_backup_base_name(self.volume_id,
|
||||||
diff_format=True)
|
self.alt_backup)
|
||||||
|
|
||||||
def mock_write_data():
|
def mock_write_data():
|
||||||
self.volume_file.seek(0)
|
self.volume_file.seek(0)
|
||||||
@ -483,8 +500,11 @@ class BackupCephTestCase(test.TestCase):
|
|||||||
{'name': 'backup.mock.snap.15341241.90'},
|
{'name': 'backup.mock.snap.15341241.90'},
|
||||||
{'name': 'backup.mock.snap.199994362.10'}])
|
{'name': 'backup.mock.snap.199994362.10'}])
|
||||||
|
|
||||||
output = self.service.backup(self.backup, rbdio)
|
output = self.service.backup(self.alt_backup,
|
||||||
self.assertDictEqual({}, output)
|
rbdio)
|
||||||
|
base_name = '{"base": "%s"}' % backup_name
|
||||||
|
service_meta = {'service_metadata': base_name}
|
||||||
|
self.assertDictEqual(service_meta, output)
|
||||||
|
|
||||||
self.assertEqual(['popen_init',
|
self.assertEqual(['popen_init',
|
||||||
'read',
|
'read',
|
||||||
@ -494,7 +514,7 @@ class BackupCephTestCase(test.TestCase):
|
|||||||
'communicate'], self.callstack)
|
'communicate'], self.callstack)
|
||||||
|
|
||||||
self.assertFalse(mock_full_backup.called)
|
self.assertFalse(mock_full_backup.called)
|
||||||
self.assertTrue(mock_get_backup_snaps.called)
|
self.assertFalse(mock_get_backup_snaps.called)
|
||||||
|
|
||||||
# Ensure the files are equal
|
# Ensure the files are equal
|
||||||
self.assertEqual(checksum.digest(),
|
self.assertEqual(checksum.digest(),
|
||||||
@ -505,7 +525,7 @@ class BackupCephTestCase(test.TestCase):
|
|||||||
with mock.patch.object(self.service, '_backup_rbd') as \
|
with mock.patch.object(self.service, '_backup_rbd') as \
|
||||||
mock_backup_rbd, mock.patch.object(self.service,
|
mock_backup_rbd, mock.patch.object(self.service,
|
||||||
'_backup_metadata'):
|
'_backup_metadata'):
|
||||||
mock_backup_rbd.return_value = {'parent_id': 'mock'}
|
mock_backup_rbd.return_value = {'service_metadata': 'base_name'}
|
||||||
image = self.service.rbd.Image()
|
image = self.service.rbd.Image()
|
||||||
meta = linuxrbd.RBDImageMetadata(image,
|
meta = linuxrbd.RBDImageMetadata(image,
|
||||||
'pool_foo',
|
'pool_foo',
|
||||||
@ -513,15 +533,14 @@ class BackupCephTestCase(test.TestCase):
|
|||||||
'conf_foo')
|
'conf_foo')
|
||||||
rbdio = linuxrbd.RBDVolumeIOWrapper(meta)
|
rbdio = linuxrbd.RBDVolumeIOWrapper(meta)
|
||||||
output = self.service.backup(self.backup, rbdio)
|
output = self.service.backup(self.backup, rbdio)
|
||||||
self.assertDictEqual({'parent_id': 'mock'}, output)
|
self.assertDictEqual({'service_metadata': 'base_name'}, output)
|
||||||
|
|
||||||
@common_mocks
|
@common_mocks
|
||||||
def test_backup_volume_from_rbd_set_parent_id_none(self):
|
def test_backup_volume_from_rbd_got_exception(self):
|
||||||
backup_name = self.service._get_backup_base_name(
|
base_name = self.service._get_backup_base_name(self.volume_id,
|
||||||
self.volume_id, diff_format=True)
|
self.alt_backup)
|
||||||
|
|
||||||
self.mock_rbd.RBD().list.return_value = [backup_name]
|
self.mock_rbd.RBD().list.return_value = [base_name]
|
||||||
self.backup.parent_id = 'mock_parent_id'
|
|
||||||
|
|
||||||
with mock.patch.object(self.service, 'get_backup_snaps'), \
|
with mock.patch.object(self.service, 'get_backup_snaps'), \
|
||||||
mock.patch.object(self.service, '_rbd_diff_transfer') as \
|
mock.patch.object(self.service, '_rbd_diff_transfer') as \
|
||||||
@ -551,29 +570,54 @@ class BackupCephTestCase(test.TestCase):
|
|||||||
'conf_foo')
|
'conf_foo')
|
||||||
rbdio = linuxrbd.RBDVolumeIOWrapper(meta)
|
rbdio = linuxrbd.RBDVolumeIOWrapper(meta)
|
||||||
mock_get_backup_snaps.return_value = (
|
mock_get_backup_snaps.return_value = (
|
||||||
[{'name': 'backup.mock.snap.153464362.12'},
|
[{'name': 'backup.mock.snap.153464362.12',
|
||||||
{'name': 'backup.mock.snap.199994362.10'}])
|
'backup_id': 'mock_parent_id'},
|
||||||
output = self.service.backup(self.backup, rbdio)
|
{'name': 'backup.mock.snap.199994362.10',
|
||||||
self.assertIsNone(output['parent_id'])
|
'backup_id': 'mock'}])
|
||||||
|
self.assertRaises(exception.BackupRBDOperationFailed,
|
||||||
|
self.service.backup,
|
||||||
|
self.alt_backup, rbdio)
|
||||||
|
|
||||||
@common_mocks
|
@common_mocks
|
||||||
def test_backup_rbd_set_parent_id(self):
|
def test_backup_rbd_set_parent_id(self):
|
||||||
backup_name = self.service._get_backup_base_name(
|
base_name = self.service._get_backup_base_name(self.volume_id,
|
||||||
self.volume_id, diff_format=True)
|
self.alt_backup)
|
||||||
vol_name = self.volume.name
|
vol_name = self.volume.name
|
||||||
vol_length = self.volume.size
|
vol_length = self.volume.size
|
||||||
|
|
||||||
self.mock_rbd.RBD().list.return_value = [backup_name]
|
self.mock_rbd.RBD().list.return_value = [base_name]
|
||||||
|
|
||||||
with mock.patch.object(self.service, '_snap_exists'), \
|
with mock.patch.object(self.service, '_snap_exists'), \
|
||||||
mock.patch.object(self.service, '_get_backup_base_name') as \
|
mock.patch.object(self.service, '_get_backup_snap_name') as \
|
||||||
mock_get_backup_base_name, \
|
mock_get_backup_snap_name, \
|
||||||
mock.patch.object(self.service, '_get_most_recent_snap') as \
|
|
||||||
mock_get_most_recent_snap, \
|
|
||||||
mock.patch.object(self.service, '_rbd_diff_transfer'):
|
mock.patch.object(self.service, '_rbd_diff_transfer'):
|
||||||
mock_get_backup_base_name.return_value = backup_name
|
image = self.service.rbd.Image()
|
||||||
mock_get_most_recent_snap.return_value = (
|
mock_get_backup_snap_name.return_value = 'mock_snap_name'
|
||||||
'backup.mock.snap.153464362.12')
|
meta = linuxrbd.RBDImageMetadata(image,
|
||||||
|
'pool_foo',
|
||||||
|
'user_foo',
|
||||||
|
'conf_foo')
|
||||||
|
rbdio = linuxrbd.RBDVolumeIOWrapper(meta)
|
||||||
|
rbdio.seek(0)
|
||||||
|
output = self.service._backup_rbd(self.alt_backup, rbdio,
|
||||||
|
vol_name, vol_length)
|
||||||
|
base_name = '{"base": "%s"}' % base_name
|
||||||
|
self.assertEqual({'service_metadata': base_name}, output)
|
||||||
|
self.backup.parent_id = None
|
||||||
|
|
||||||
|
@common_mocks
|
||||||
|
def test_backup_rbd_without_parent_id(self):
|
||||||
|
full_backup_name = self.service._get_backup_base_name(self.volume_id,
|
||||||
|
self.alt_backup)
|
||||||
|
vol_name = self.volume.name
|
||||||
|
vol_length = self.volume.size
|
||||||
|
|
||||||
|
with mock.patch.object(self.service, '_rbd_diff_transfer'), \
|
||||||
|
mock.patch.object(self.service, '_create_base_image') as \
|
||||||
|
mock_create_base_image, mock.patch.object(
|
||||||
|
rbd_driver, 'RADOSClient') as mock_rados_client:
|
||||||
|
client = mock.Mock()
|
||||||
|
mock_rados_client.return_value.__enter__.return_value = client
|
||||||
image = self.service.rbd.Image()
|
image = self.service.rbd.Image()
|
||||||
meta = linuxrbd.RBDImageMetadata(image,
|
meta = linuxrbd.RBDImageMetadata(image,
|
||||||
'pool_foo',
|
'pool_foo',
|
||||||
@ -581,9 +625,12 @@ class BackupCephTestCase(test.TestCase):
|
|||||||
'conf_foo')
|
'conf_foo')
|
||||||
rbdio = linuxrbd.RBDVolumeIOWrapper(meta)
|
rbdio = linuxrbd.RBDVolumeIOWrapper(meta)
|
||||||
rbdio.seek(0)
|
rbdio.seek(0)
|
||||||
output = self.service._backup_rbd(self.backup, rbdio,
|
output = self.service._backup_rbd(self.alt_backup, rbdio,
|
||||||
vol_name, vol_length)
|
vol_name, vol_length)
|
||||||
self.assertDictEqual({'parent_id': 'mock'}, output)
|
mock_create_base_image.assert_called_with(full_backup_name,
|
||||||
|
vol_length, client)
|
||||||
|
base_name = '{"base": "%s"}' % full_backup_name
|
||||||
|
self.assertEqual({'service_metadata': base_name}, output)
|
||||||
|
|
||||||
@common_mocks
|
@common_mocks
|
||||||
@mock.patch('fcntl.fcntl', spec=True)
|
@mock.patch('fcntl.fcntl', spec=True)
|
||||||
@ -597,7 +644,7 @@ class BackupCephTestCase(test.TestCase):
|
|||||||
self._try_delete_base_image().
|
self._try_delete_base_image().
|
||||||
"""
|
"""
|
||||||
backup_name = self.service._get_backup_base_name(self.volume_id,
|
backup_name = self.service._get_backup_base_name(self.volume_id,
|
||||||
diff_format=True)
|
self.alt_backup)
|
||||||
|
|
||||||
def mock_write_data():
|
def mock_write_data():
|
||||||
self.volume_file.seek(0)
|
self.volume_file.seek(0)
|
||||||
@ -662,7 +709,7 @@ class BackupCephTestCase(test.TestCase):
|
|||||||
self.assertRaises(
|
self.assertRaises(
|
||||||
self.service.rbd.ImageNotFound,
|
self.service.rbd.ImageNotFound,
|
||||||
self.service.backup,
|
self.service.backup,
|
||||||
self.backup, rbdio)
|
self.alt_backup, rbdio)
|
||||||
|
|
||||||
@common_mocks
|
@common_mocks
|
||||||
@mock.patch('fcntl.fcntl', spec=True)
|
@mock.patch('fcntl.fcntl', spec=True)
|
||||||
@ -675,7 +722,7 @@ class BackupCephTestCase(test.TestCase):
|
|||||||
second exception occurs in self.delete_backup().
|
second exception occurs in self.delete_backup().
|
||||||
"""
|
"""
|
||||||
backup_name = self.service._get_backup_base_name(self.volume_id,
|
backup_name = self.service._get_backup_base_name(self.volume_id,
|
||||||
diff_format=True)
|
self.alt_backup)
|
||||||
|
|
||||||
def mock_write_data():
|
def mock_write_data():
|
||||||
self.volume_file.seek(0)
|
self.volume_file.seek(0)
|
||||||
@ -733,12 +780,11 @@ class BackupCephTestCase(test.TestCase):
|
|||||||
self.assertRaises(
|
self.assertRaises(
|
||||||
self.service.rbd.ImageBusy,
|
self.service.rbd.ImageBusy,
|
||||||
self.service.backup,
|
self.service.backup,
|
||||||
self.backup, rbdio)
|
self.alt_backup, rbdio)
|
||||||
|
|
||||||
@common_mocks
|
@common_mocks
|
||||||
def test_backup_rbd_from_snap(self):
|
def test_backup_rbd_from_snap(self):
|
||||||
backup_name = self.service._get_backup_base_name(self.volume_id,
|
backup_name = self.service._get_backup_base_name(self.volume_id)
|
||||||
diff_format=True)
|
|
||||||
vol_name = self.volume['name']
|
vol_name = self.volume['name']
|
||||||
vol_length = self.service._get_volume_size_gb(self.volume)
|
vol_length = self.service._get_volume_size_gb(self.volume)
|
||||||
|
|
||||||
@ -780,44 +826,36 @@ class BackupCephTestCase(test.TestCase):
|
|||||||
|
|
||||||
@common_mocks
|
@common_mocks
|
||||||
def test_backup_rbd_from_snap2(self):
|
def test_backup_rbd_from_snap2(self):
|
||||||
backup_name = self.service._get_backup_base_name(self.volume_id,
|
base_name = self.service._get_backup_base_name(self.volume_id,
|
||||||
diff_format=True)
|
self.alt_backup)
|
||||||
vol_name = self.volume['name']
|
vol_name = self.volume['name']
|
||||||
vol_length = self.service._get_volume_size_gb(self.volume)
|
vol_length = self.service._get_volume_size_gb(self.volume)
|
||||||
|
|
||||||
self.mock_rbd.RBD().list = mock.Mock()
|
self.mock_rbd.RBD().list = mock.Mock()
|
||||||
self.mock_rbd.RBD().list.return_value = [backup_name]
|
self.mock_rbd.RBD().list.return_value = [base_name]
|
||||||
|
|
||||||
with mock.patch.object(self.service, '_get_most_recent_snap') as \
|
with mock.patch.object(self.service, '_get_backup_base_name') as \
|
||||||
mock_get_most_recent_snap:
|
mock_get_backup_base_name:
|
||||||
with mock.patch.object(self.service, '_get_backup_base_name') as \
|
with mock.patch.object(self.service, '_rbd_diff_transfer') as \
|
||||||
mock_get_backup_base_name:
|
mock_rbd_diff_transfer:
|
||||||
with mock.patch.object(self.service, '_rbd_diff_transfer') as \
|
with mock.patch.object(self.service, '_get_new_snap_name') as \
|
||||||
mock_rbd_diff_transfer:
|
mock_get_new_snap_name:
|
||||||
with mock.patch.object(self.service,
|
mock_get_backup_base_name.return_value = base_name
|
||||||
'_get_new_snap_name') as \
|
mock_get_new_snap_name.return_value = 'new_snap'
|
||||||
mock_get_new_snap_name:
|
image = self.service.rbd.Image()
|
||||||
mock_get_backup_base_name.return_value = (
|
meta = linuxrbd.RBDImageMetadata(image, 'pool_foo',
|
||||||
backup_name)
|
'user_foo', 'conf_foo')
|
||||||
mock_get_most_recent_snap.return_value = (
|
rbdio = linuxrbd.RBDVolumeIOWrapper(meta)
|
||||||
'backup.mock.snap.153464362.12')
|
rbdio.seek(0)
|
||||||
mock_get_new_snap_name.return_value = 'new_snap'
|
self.service._backup_rbd(self.alt_backup, rbdio, vol_name,
|
||||||
image = self.service.rbd.Image()
|
vol_length)
|
||||||
meta = linuxrbd.RBDImageMetadata(image,
|
mock_rbd_diff_transfer.assert_called_with(
|
||||||
'pool_foo',
|
vol_name, 'pool_foo', base_name,
|
||||||
'user_foo',
|
self.backup.container, src_user='user_foo',
|
||||||
'conf_foo')
|
src_conf='conf_foo',
|
||||||
rbdio = linuxrbd.RBDVolumeIOWrapper(meta)
|
dest_conf='/etc/ceph/ceph.conf',
|
||||||
rbdio.seek(0)
|
dest_user='cinder', src_snap='new_snap',
|
||||||
self.service._backup_rbd(self.backup, rbdio,
|
from_snap=None)
|
||||||
vol_name, vol_length)
|
|
||||||
mock_rbd_diff_transfer.assert_called_with(
|
|
||||||
vol_name, 'pool_foo', backup_name,
|
|
||||||
self.backup.container, src_user='user_foo',
|
|
||||||
src_conf='conf_foo',
|
|
||||||
dest_conf='/etc/ceph/ceph.conf',
|
|
||||||
dest_user='cinder', src_snap='new_snap',
|
|
||||||
from_snap='backup.mock.snap.153464362.12')
|
|
||||||
|
|
||||||
@common_mocks
|
@common_mocks
|
||||||
def test_backup_vol_length_0(self):
|
def test_backup_vol_length_0(self):
|
||||||
@ -848,7 +886,7 @@ class BackupCephTestCase(test.TestCase):
|
|||||||
@common_mocks
|
@common_mocks
|
||||||
def test_restore(self):
|
def test_restore(self):
|
||||||
backup_name = self.service._get_backup_base_name(self.volume_id,
|
backup_name = self.service._get_backup_base_name(self.volume_id,
|
||||||
diff_format=True)
|
self.alt_backup)
|
||||||
|
|
||||||
self.mock_rbd.RBD.return_value.list.return_value = [backup_name]
|
self.mock_rbd.RBD.return_value.list.return_value = [backup_name]
|
||||||
|
|
||||||
@ -870,7 +908,7 @@ class BackupCephTestCase(test.TestCase):
|
|||||||
with tempfile.NamedTemporaryFile() as test_file:
|
with tempfile.NamedTemporaryFile() as test_file:
|
||||||
self.volume_file.seek(0)
|
self.volume_file.seek(0)
|
||||||
|
|
||||||
self.service.restore(self.backup, self.volume_id,
|
self.service.restore(self.alt_backup, self.volume_id,
|
||||||
test_file)
|
test_file)
|
||||||
|
|
||||||
checksum = hashlib.sha256()
|
checksum = hashlib.sha256()
|
||||||
@ -965,8 +1003,7 @@ class BackupCephTestCase(test.TestCase):
|
|||||||
@common_mocks
|
@common_mocks
|
||||||
def test_delete_backup_snapshot(self):
|
def test_delete_backup_snapshot(self):
|
||||||
snap_name = 'backup.%s.snap.3824923.1412' % (uuid.uuid4())
|
snap_name = 'backup.%s.snap.3824923.1412' % (uuid.uuid4())
|
||||||
base_name = self.service._get_backup_base_name(self.volume_id,
|
base_name = self.service._get_backup_base_name(self.volume_id)
|
||||||
diff_format=True)
|
|
||||||
self.mock_rbd.RBD.remove_snap = mock.Mock()
|
self.mock_rbd.RBD.remove_snap = mock.Mock()
|
||||||
thread_dict = {}
|
thread_dict = {}
|
||||||
|
|
||||||
@ -996,16 +1033,16 @@ class BackupCephTestCase(test.TestCase):
|
|||||||
@mock.patch('cinder.backup.drivers.ceph.VolumeMetadataBackup', spec=True)
|
@mock.patch('cinder.backup.drivers.ceph.VolumeMetadataBackup', spec=True)
|
||||||
def test_try_delete_base_image_diff_format(self, mock_meta_backup):
|
def test_try_delete_base_image_diff_format(self, mock_meta_backup):
|
||||||
backup_name = self.service._get_backup_base_name(self.volume_id,
|
backup_name = self.service._get_backup_base_name(self.volume_id,
|
||||||
diff_format=True)
|
self.alt_backup)
|
||||||
|
|
||||||
self.mock_rbd.RBD.return_value.list.return_value = [backup_name]
|
self.mock_rbd.RBD.return_value.list.return_value = [backup_name]
|
||||||
|
|
||||||
with mock.patch.object(self.service, '_delete_backup_snapshot') as \
|
with mock.patch.object(self.service, '_delete_backup_snapshot') as \
|
||||||
mock_del_backup_snap:
|
mock_del_backup_snap:
|
||||||
snap_name = self.service._get_new_snap_name(self.backup_id)
|
snap_name = self.service._get_new_snap_name(self.alt_backup_id)
|
||||||
mock_del_backup_snap.return_value = (snap_name, 0)
|
mock_del_backup_snap.return_value = (snap_name, 0)
|
||||||
|
|
||||||
self.service.delete_backup(self.backup)
|
self.service.delete_backup(self.alt_backup)
|
||||||
self.assertTrue(mock_del_backup_snap.called)
|
self.assertTrue(mock_del_backup_snap.called)
|
||||||
|
|
||||||
self.assertTrue(self.mock_rbd.RBD.return_value.list.called)
|
self.assertTrue(self.mock_rbd.RBD.return_value.list.called)
|
||||||
@ -1015,7 +1052,7 @@ class BackupCephTestCase(test.TestCase):
|
|||||||
@mock.patch('cinder.backup.drivers.ceph.VolumeMetadataBackup', spec=True)
|
@mock.patch('cinder.backup.drivers.ceph.VolumeMetadataBackup', spec=True)
|
||||||
def test_try_delete_base_image(self, mock_meta_backup):
|
def test_try_delete_base_image(self, mock_meta_backup):
|
||||||
backup_name = self.service._get_backup_base_name(self.volume_id,
|
backup_name = self.service._get_backup_base_name(self.volume_id,
|
||||||
self.backup_id)
|
self.alt_backup)
|
||||||
thread_dict = {}
|
thread_dict = {}
|
||||||
|
|
||||||
def mock_side_effect(ioctx, base_name):
|
def mock_side_effect(ioctx, base_name):
|
||||||
@ -1024,7 +1061,7 @@ class BackupCephTestCase(test.TestCase):
|
|||||||
self.mock_rbd.RBD.return_value.list.return_value = [backup_name]
|
self.mock_rbd.RBD.return_value.list.return_value = [backup_name]
|
||||||
self.mock_rbd.RBD.return_value.remove.side_effect = mock_side_effect
|
self.mock_rbd.RBD.return_value.remove.side_effect = mock_side_effect
|
||||||
with mock.patch.object(self.service, 'get_backup_snaps'):
|
with mock.patch.object(self.service, 'get_backup_snaps'):
|
||||||
self.service.delete_backup(self.backup)
|
self.service.delete_backup(self.alt_backup)
|
||||||
self.assertTrue(self.mock_rbd.RBD.return_value.remove.called)
|
self.assertTrue(self.mock_rbd.RBD.return_value.remove.called)
|
||||||
self.assertNotEqual(threading.current_thread(),
|
self.assertNotEqual(threading.current_thread(),
|
||||||
thread_dict['thread'])
|
thread_dict['thread'])
|
||||||
@ -1033,7 +1070,7 @@ class BackupCephTestCase(test.TestCase):
|
|||||||
def test_try_delete_base_image_busy(self):
|
def test_try_delete_base_image_busy(self):
|
||||||
"""This should induce retries then raise rbd.ImageBusy."""
|
"""This should induce retries then raise rbd.ImageBusy."""
|
||||||
backup_name = self.service._get_backup_base_name(self.volume_id,
|
backup_name = self.service._get_backup_base_name(self.volume_id,
|
||||||
self.backup_id)
|
self.alt_backup)
|
||||||
|
|
||||||
rbd = self.mock_rbd.RBD.return_value
|
rbd = self.mock_rbd.RBD.return_value
|
||||||
rbd.list.return_value = [backup_name]
|
rbd.list.return_value = [backup_name]
|
||||||
@ -1043,7 +1080,7 @@ class BackupCephTestCase(test.TestCase):
|
|||||||
mock_get_backup_snaps:
|
mock_get_backup_snaps:
|
||||||
self.assertRaises(self.mock_rbd.ImageBusy,
|
self.assertRaises(self.mock_rbd.ImageBusy,
|
||||||
self.service._try_delete_base_image,
|
self.service._try_delete_base_image,
|
||||||
self.backup)
|
self.alt_backup)
|
||||||
self.assertTrue(mock_get_backup_snaps.called)
|
self.assertTrue(mock_get_backup_snaps.called)
|
||||||
|
|
||||||
self.assertTrue(rbd.list.called)
|
self.assertTrue(rbd.list.called)
|
||||||
|
@ -23,9 +23,9 @@ from cinder import test
|
|||||||
# NOTE: The hashes in this list should only be changed if they come with a
|
# NOTE: The hashes in this list should only be changed if they come with a
|
||||||
# corresponding version bump in the affected objects.
|
# corresponding version bump in the affected objects.
|
||||||
object_data = {
|
object_data = {
|
||||||
'Backup': '1.6-c7ede487ba6fbcdd2a4711343cd972be',
|
'Backup': '1.7-fffdbcd5da3c30750916fa2cc0e8ffb5',
|
||||||
'BackupDeviceInfo': '1.0-74b3950676c690538f4bc6796bd0042e',
|
'BackupDeviceInfo': '1.0-74b3950676c690538f4bc6796bd0042e',
|
||||||
'BackupImport': '1.6-c7ede487ba6fbcdd2a4711343cd972be',
|
'BackupImport': '1.7-fffdbcd5da3c30750916fa2cc0e8ffb5',
|
||||||
'BackupList': '1.0-15ecf022a68ddbb8c2a6739cfc9f8f5e',
|
'BackupList': '1.0-15ecf022a68ddbb8c2a6739cfc9f8f5e',
|
||||||
'CleanupRequest': '1.0-e7c688b893e1d5537ccf65cc3eb10a28',
|
'CleanupRequest': '1.0-e7c688b893e1d5537ccf65cc3eb10a28',
|
||||||
'Cluster': '1.1-e2c533eb8cdd8d229b6c45c6cf3a9e2c',
|
'Cluster': '1.1-e2c533eb8cdd8d229b6c45c6cf3a9e2c',
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
fixes:
|
||||||
|
- Fixed issue where all Ceph RBD backups would be incremental after the
|
||||||
|
first one. The driver now honors whether ``--incremental`` is specified or
|
||||||
|
not.
|
Loading…
x
Reference in New Issue
Block a user