Skip to content

Commit

Permalink
Merge "Support Incremental Backup Completion In RBD" into stable/rocky
Browse files Browse the repository at this point in the history
  • Loading branch information
Zuul authored and openstack-gerrit committed Feb 22, 2020
2 parents a47492f + bce6d01 commit 7068da1
Show file tree
Hide file tree
Showing 7 changed files with 292 additions and 208 deletions.
4 changes: 4 additions & 0 deletions cinder/backup/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,10 @@ def create(self, context, name, description, volume_id,
raise exception.InvalidBackup(reason=msg)

parent_id = None
parent = None

if latest_backup:
parent = latest_backup
parent_id = latest_backup.id
if latest_backup['status'] != fields.BackupStatus.AVAILABLE:
msg = _('The parent backup must be available for '
Expand Down Expand Up @@ -313,6 +316,7 @@ def create(self, context, name, description, volume_id,
'availability_zone': availability_zone,
'snapshot_id': snapshot_id,
'data_timestamp': data_timestamp,
'parent': parent,
'metadata': metadata or {}
}
backup = objects.Backup(context=context, **kwargs)
Expand Down
220 changes: 122 additions & 98 deletions cinder/backup/drivers/ceph.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"""

import fcntl
import json
import os
import re
import subprocess
Expand Down Expand Up @@ -310,22 +311,39 @@ def _disconnect_from_rados(self, client, ioctx):
ioctx.close()
client.shutdown()

def _get_backup_base_name(self, volume_id, backup_id=None,
diff_format=False):
def _format_base_name(self, service_metadata):
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.
Incremental backups use a new base name so we support old and new style
format.
"""
# Ensure no unicode
if diff_format:
if not backup:
return utils.convert_str("volume-%s.backup.base" % volume_id)
else:
if backup_id is None:
msg = _("Backup id required")
raise exception.InvalidParameterValue(msg)
return utils.convert_str("volume-%s.backup.%s"
% (volume_id, backup_id))

if backup.service_metadata:
return self._format_base_name(backup.service_metadata)

# 'parent' field will only be present in incremental backups. This is
# 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):
"""Trim length bytes from offset.
Expand Down Expand Up @@ -467,7 +485,7 @@ def _try_delete_base_image(self, backup, base_name=None):
if base_name is None:
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 "
"backup base image of volume %(volume)s.",
{'basename': base_name, 'volume': volume_id})
Expand Down Expand Up @@ -616,7 +634,7 @@ def _rbd_image_exists(self, name, volume_id, client,
if name not in rbds:
LOG.debug("Image '%s' not found - trying diff format name", name)
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:
LOG.debug("Diff format image '%s' not found", name)
return False, name
Expand All @@ -643,50 +661,79 @@ def _snap_exists(self, base_name, snap_name, client):

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):
"""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_pool = volume_file.rbd_pool
rbd_conf = volume_file.rbd_conf
source_rbd_image = eventlet.tpool.Proxy(volume_file.rbd_image)
volume_id = backup.volume_id
updates = {}
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
self._create_base_image(base_name, length, client)
image_created = True
else:
# If a from_snap is defined and is present in the source volume
# image but does not exist in the backup base then we look down
# the list of source volume snapshots and find the latest one
# for which a backup snapshot exist in the backup base. Until
# that snapshot is reached, we delete all the other snapshots
# for which backup snapshot does not exist.
from_snap = self._get_most_recent_snap(source_rbd_image,
base_name, client)
base_name = None

# If backup.parent_id is None performs full RBD backup
if backup.parent_id is None:
base_name = self._get_backup_base_name(volume_id, backup=backup)
from_snap, image_created = self._full_rbd_backup(backup.container,
base_name,
length)
# Otherwise performs incremental rbd backup
else:
# Find the base name from the parent backup's service_metadata
base_name = self._get_backup_base_name(volume_id, backup=backup)
rbd_img = source_rbd_image
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 "
"volume %(volume)s.",
Expand Down Expand Up @@ -730,14 +777,8 @@ def _backup_rbd(self, backup, volume_file, volume_name, length):
"source volume='%(volume)s'.",
{'snapshot': new_snap, 'volume': volume_id})
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
# 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

return {'service_metadata': '{"base": "%s"}' % base_name}

def _file_is_rbd(self, volume_file):
"""Returns True if the volume_file is actually an RBD image."""
Expand All @@ -751,7 +792,7 @@ def _full_backup(self, backup, src_volume, src_name, length):
image.
"""
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,
backup.container)) as client:
Expand Down Expand Up @@ -854,23 +895,6 @@ def _get_backup_snap_name(self, rbd_image, name, backup_id):
LOG.debug("Found snapshot '%s'", 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):
"""Return the size in gigabytes of the given volume.
Expand Down Expand Up @@ -924,17 +948,23 @@ def backup(self, backup, volume_file, backup_metadata=True):
volume_file.seek(0)
length = self._get_volume_size_gb(volume)

do_full_backup = False
if self._file_is_rbd(volume_file):
# If volume an RBD, attempt incremental backup.
LOG.debug("Volume file is RBD: attempting incremental backup.")
if backup.snapshot_id:
do_full_backup = True
elif self._file_is_rbd(volume_file):
# If volume an RBD, attempt incremental or full backup.
do_full_backup = False
LOG.debug("Volume file is RBD: attempting optimized backup")
try:
updates = self._backup_rbd(backup, volume_file,
volume.name, length)
updates = self._backup_rbd(backup, volume_file, volume.name,
length)
except exception.BackupRBDOperationFailed:
LOG.debug("Forcing full backup of volume %s.", volume.id)
do_full_backup = True
with excutils.save_and_reraise_exception():
self.delete_backup(backup)
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.")
do_full_backup = True

Expand All @@ -956,11 +986,6 @@ def backup(self, backup, volume_file, backup_metadata=True):
LOG.debug("Backup '%(backup_id)s' of volume %(volume_id)s finished.",
{'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

def _full_restore(self, backup, dest_file, dest_name, length,
Expand All @@ -975,13 +1000,10 @@ def _full_restore(self, backup, dest_file, dest_name, length,
# If a source snapshot is provided we assume the base is diff
# format.
if src_snap:
diff_format = True
backup_name = self._get_backup_base_name(backup.volume_id,
backup=backup)
else:
diff_format = False

backup_name = self._get_backup_base_name(backup.volume_id,
backup_id=backup.id,
diff_format=diff_format)
backup_name = self._get_backup_base_name(backup.volume_id)

# Retrieve backup volume
src_rbd = eventlet.tpool.Proxy(self.rbd.Image(client.ioctx,
Expand All @@ -1008,7 +1030,7 @@ def _check_restore_vol_size(self, backup, restore_vol, restore_length,
post-process and resize it back to its expected size.
"""
backup_base = self._get_backup_base_name(backup.volume_id,
diff_format=True)
backup=backup)

with eventlet.tpool.Proxy(rbd_driver.RADOSClient(self,
backup.container)) as client:
Expand All @@ -1033,7 +1055,7 @@ def _diff_restore_rbd(self, backup, restore_file, restore_name,
rbd_pool = restore_file.rbd_pool
rbd_conf = restore_file.rbd_conf
base_name = self._get_backup_base_name(backup.volume_id,
diff_format=True)
backup=backup)

LOG.debug("Attempting incremental restore from base='%(base)s' "
"snap='%(snap)s'",
Expand Down Expand Up @@ -1165,8 +1187,10 @@ def _restore_volume(self, backup, volume, volume_file):
"""
length = int(volume.size) * units.Gi

base_name = self._get_backup_base_name(backup.volume_id,
diff_format=True)
if backup.service_metadata:
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(
self, backup.container)) as client:
Expand Down
11 changes: 10 additions & 1 deletion cinder/objects/backup.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,8 @@ class Backup(base.CinderPersistentObject, base.CinderObject,
# Version 1.4: Add restore_volume_id
# Version 1.5: Add metadata
# Version 1.6: Add encryption_key_id
VERSION = '1.6'
# Version 1.7: Add parent
VERSION = '1.7'

OPTIONAL_FIELDS = ('metadata',)

Expand All @@ -55,6 +56,7 @@ class Backup(base.CinderPersistentObject, base.CinderObject,
'availability_zone': fields.StringField(nullable=True),
'container': fields.StringField(nullable=True),
'parent_id': fields.StringField(nullable=True),
'parent': fields.ObjectField('Backup', nullable=True),
'status': c_fields.BackupStatusField(nullable=True),
'fail_reason': fields.StringField(nullable=True),
'size': fields.IntegerField(nullable=True),
Expand Down Expand Up @@ -110,8 +112,14 @@ def has_dependent_backups(self):

def obj_make_compatible(self, primitive, 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)
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
def _from_db_object(cls, context, backup, db_backup, expected_attrs=None):
Expand Down Expand Up @@ -174,6 +182,7 @@ def save(self):
self.metadata = db.backup_metadata_update(self._context,
self.id, metadata,
True)
updates.pop('parent', None)
db.backup_update(self._context, self.id, updates)

self.obj_reset_changes()
Expand Down
Loading

0 comments on commit 7068da1

Please sign in to comment.