# Copyright (C) 2012 Hewlett-Packard Development Company, L.P. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ Backup manager manages volume backups. Volume Backups are full copies of persistent volumes stored in a backup store e.g. an object store or any other backup store if and when support is added. They are usable without the original object being available. A volume backup can be restored to the original volume it was created from or any other available volume with a minimum size of the original volume. Volume backups can be created, restored, deleted and listed. **Related Flags** :backup_topic: What :mod:`rpc` topic to listen to (default: `cinder-backup`). :backup_manager: The module name of a class derived from :class:`manager.Manager` (default: :class:`cinder.backup.manager.Manager`). """ from oslo_config import cfg from oslo_log import log as logging import oslo_messaging as messaging from oslo_utils import excutils from oslo_utils import importutils import six from cinder.backup import driver from cinder.backup import rpcapi as backup_rpcapi from cinder import context from cinder import exception from cinder.i18n import _, _LE, _LI, _LW from cinder import manager from cinder import objects from cinder import quota from cinder import rpc from cinder import utils from cinder.volume import utils as volume_utils LOG = logging.getLogger(__name__) backup_manager_opts = [ cfg.StrOpt('backup_driver', default='cinder.backup.drivers.swift', help='Driver to use for backups.',) ] # This map doesn't need to be extended in the future since it's only # for old backup services mapper = {'cinder.backup.services.swift': 'cinder.backup.drivers.swift', 'cinder.backup.services.ceph': 'cinder.backup.drivers.ceph'} CONF = cfg.CONF CONF.register_opts(backup_manager_opts) QUOTAS = quota.QUOTAS class BackupManager(manager.SchedulerDependentManager): """Manages backup of block storage devices.""" RPC_API_VERSION = '1.2' target = messaging.Target(version=RPC_API_VERSION) def __init__(self, service_name=None, *args, **kwargs): self.service = importutils.import_module(self.driver_name) self.az = CONF.storage_availability_zone self.volume_managers = {} self._setup_volume_drivers() self.backup_rpcapi = backup_rpcapi.BackupAPI() super(BackupManager, self).__init__(service_name='backup', *args, **kwargs) @property def driver_name(self): """This function maps old backup services to backup drivers.""" return self._map_service_to_driver(CONF.backup_driver) def _map_service_to_driver(self, service): """Maps services to drivers.""" if service in mapper: return mapper[service] return service @property def driver(self): return self._get_driver() def _get_volume_backend(self, host=None, allow_null_host=False): if host is None: if not allow_null_host: msg = _("NULL host not allowed for volume backend lookup.") raise exception.BackupFailedToGetVolumeBackend(msg) else: LOG.debug("Checking hostname '%s' for backend info.", host) part = host.partition('@') if (part[1] == '@') and (part[2] != ''): backend = part[2] LOG.debug("Got backend '%s'.", backend) return backend LOG.info(_LI("Backend not found in hostname (%s) so using default."), host) if 'default' not in self.volume_managers: # For multi-backend we just pick the top of the list. return self.volume_managers.keys()[0] return 'default' def _get_manager(self, backend): LOG.debug("Manager requested for volume_backend '%s'.", backend) if backend is None: LOG.debug("Fetching default backend.") backend = self._get_volume_backend(allow_null_host=True) if backend not in self.volume_managers: msg = (_("Volume manager for backend '%s' does not exist.") % (backend)) raise exception.BackupFailedToGetVolumeBackend(msg) return self.volume_managers[backend] def _get_driver(self, backend=None): LOG.debug("Driver requested for volume_backend '%s'.", backend) if backend is None: LOG.debug("Fetching default backend.") backend = self._get_volume_backend(allow_null_host=True) mgr = self._get_manager(backend) mgr.driver.db = self.db return mgr.driver def _setup_volume_drivers(self): if CONF.enabled_backends: for backend in CONF.enabled_backends: host = "%s@%s" % (CONF.host, backend) mgr = importutils.import_object(CONF.volume_manager, host=host, service_name=backend) config = mgr.configuration backend_name = config.safe_get('volume_backend_name') LOG.debug("Registering backend %(backend)s (host=%(host)s " "backend_name=%(backend_name)s).", {'backend': backend, 'host': host, 'backend_name': backend_name}) self.volume_managers[backend] = mgr else: default = importutils.import_object(CONF.volume_manager) LOG.debug("Registering default backend %s.", default) self.volume_managers['default'] = default def _init_volume_driver(self, ctxt, driver): LOG.info(_LI("Starting volume driver %(driver_name)s (%(version)s)."), {'driver_name': driver.__class__.__name__, 'version': driver.get_version()}) try: driver.do_setup(ctxt) driver.check_for_setup_error() except Exception: LOG.exception(_LE("Error encountered during initialization of " "driver: %(name)s."), {'name': driver.__class__.__name__}) # we don't want to continue since we failed # to initialize the driver correctly. return driver.set_initialized() def _update_backup_error(self, backup, context, err): backup.status = 'error' backup.fail_reason = err backup.save() def init_host(self): """Run initialization needed for a standalone service.""" ctxt = context.get_admin_context() for mgr in self.volume_managers.values(): self._init_volume_driver(ctxt, mgr.driver) LOG.info(_LI("Cleaning up incomplete backup operations.")) volumes = self.db.volume_get_all_by_host(ctxt, self.host) for volume in volumes: volume_host = volume_utils.extract_host(volume['host'], 'backend') backend = self._get_volume_backend(host=volume_host) mgr = self._get_manager(backend) if volume['status'] == 'backing-up': self._detach_all_attachments(ctxt, mgr, volume) LOG.info(_LI('Resetting volume %(vol_id)s to previous ' 'status %(status)s (was backing-up).'), {'vol_id': volume['id'], 'status': volume['previous_status']}) self.db.volume_update(ctxt, volume['id'], {'status': volume['previous_status']}) elif volume['status'] == 'restoring-backup': self._detach_all_attachments(ctxt, mgr, volume) LOG.info(_LI('setting volume %s to error_restoring ' '(was restoring-backup).'), volume['id']) self.db.volume_update(ctxt, volume['id'], {'status': 'error_restoring'}) # TODO(smulcahy) implement full resume of backup and restore # operations on restart (rather than simply resetting) backups = objects.BackupList.get_all_by_host(ctxt, self.host) for backup in backups: if backup['status'] == 'creating': LOG.info(_LI('Resetting backup %s to error (was creating).'), backup['id']) err = 'incomplete backup reset on manager restart' self._update_backup_error(backup, ctxt, err) if backup['status'] == 'restoring': LOG.info(_LI('Resetting backup %s to ' 'available (was restoring).'), backup['id']) backup.status = 'available' backup.save() if backup['status'] == 'deleting': LOG.info(_LI('Resuming delete on backup: %s.'), backup['id']) self.delete_backup(ctxt, backup) self._cleanup_temp_volumes_snapshots(backups) def _detach_all_attachments(self, ctxt, mgr, volume): attachments = volume['volume_attachment'] or [] for attachment in attachments: if (attachment['attached_host'] == self.host and attachment['instance_uuid'] is None): mgr.detach_volume(ctxt, volume['id'], attachment['id']) def _cleanup_temp_volumes_snapshots(self, backups): # NOTE(xyang): If the service crashes or gets restarted during the # backup operation, there could be temporary volumes or snapshots # that are not deleted. Make sure any temporary volumes or snapshots # create by the backup job are deleted when service is started. ctxt = context.get_admin_context() for backup in backups: try: volume = self.db.volume_get(ctxt, backup.volume_id) volume_host = volume_utils.extract_host(volume['host'], 'backend') backend = self._get_volume_backend(host=volume_host) mgr = self._get_manager(backend) except (KeyError, exception.VolumeNotFound): LOG.debug("Could not find a volume to clean up for " "backup %s.", backup.id) continue if backup.temp_volume_id and backup.status == 'error': try: temp_volume = self.db.volume_get(ctxt, backup.temp_volume_id) # The temp volume should be deleted directly thru the # the volume driver, not thru the volume manager. mgr.driver.delete_volume(temp_volume) self.db.volume_destroy(ctxt, temp_volume['id']) except exception.VolumeNotFound: LOG.debug("Could not find temp volume %(vol)s to clean up " "for backup %(backup)s.", {'vol': backup.temp_volume_id, 'backup': backup.id}) backup.temp_volume_id = None backup.save() if backup.temp_snapshot_id and backup.status == 'error': try: temp_snapshot = objects.Snapshot.get_by_id( ctxt, backup.temp_snapshot_id) # The temp snapshot should be deleted directly thru the # volume driver, not thru the volume manager. mgr.driver.delete_snapshot(temp_snapshot) with temp_snapshot.obj_as_admin(): self.db.volume_glance_metadata_delete_by_snapshot( ctxt, temp_snapshot.id) temp_snapshot.destroy() except exception.SnapshotNotFound: LOG.debug("Could not find temp snapshot %(snap)s to clean " "up for backup %(backup)s.", {'snap': backup.temp_snapshot_id, 'backup': backup.id}) backup.temp_snapshot_id = None backup.save() def create_backup(self, context, backup): """Create volume backups using configured backup service.""" volume_id = backup.volume_id volume = self.db.volume_get(context, volume_id) previous_status = volume.get('previous_status', None) LOG.info(_LI('Create backup started, backup: %(backup_id)s ' 'volume: %(volume_id)s.'), {'backup_id': backup.id, 'volume_id': volume_id}) self._notify_about_backup_usage(context, backup, "create.start") volume_host = volume_utils.extract_host(volume['host'], 'backend') backend = self._get_volume_backend(host=volume_host) backup.host = self.host backup.service = self.driver_name backup.save() expected_status = 'backing-up' actual_status = volume['status'] if actual_status != expected_status: err = _('Create backup aborted, expected volume status ' '%(expected_status)s but got %(actual_status)s.') % { 'expected_status': expected_status, 'actual_status': actual_status, } self._update_backup_error(backup, context, err) raise exception.InvalidVolume(reason=err) expected_status = 'creating' actual_status = backup.status if actual_status != expected_status: err = _('Create backup aborted, expected backup status ' '%(expected_status)s but got %(actual_status)s.') % { 'expected_status': expected_status, 'actual_status': actual_status, } self._update_backup_error(backup, context, err) backup.save() raise exception.InvalidBackup(reason=err) try: # NOTE(flaper87): Verify the driver is enabled # before going forward. The exception will be caught, # the volume status will be set back to available and # the backup status to 'error' utils.require_driver_initialized(self.driver) backup_service = self.service.get_backup_driver(context) self._get_driver(backend).backup_volume(context, backup, backup_service) except Exception as err: with excutils.save_and_reraise_exception(): self.db.volume_update(context, volume_id, {'status': previous_status, 'previous_status': 'error_backing-up'}) self._update_backup_error(backup, context, six.text_type(err)) # Restore the original status. self.db.volume_update(context, volume_id, {'status': previous_status, 'previous_status': 'backing-up'}) backup.status = 'available' backup.size = volume['size'] backup.availability_zone = self.az backup.save() LOG.info(_LI('Create backup finished. backup: %s.'), backup.id) self._notify_about_backup_usage(context, backup, "create.end") def restore_backup(self, context, backup, volume_id): """Restore volume backups from configured backup service.""" LOG.info(_LI('Restore backup started, backup: %(backup_id)s ' 'volume: %(volume_id)s.'), {'backup_id': backup.id, 'volume_id': volume_id}) volume = self.db.volume_get(context, volume_id) volume_host = volume_utils.extract_host(volume['host'], 'backend') backend = self._get_volume_backend(host=volume_host) self._notify_about_backup_usage(context, backup, "restore.start") backup.host = self.host backup.save() expected_status = 'restoring-backup' actual_status = volume['status'] if actual_status != expected_status: err = (_('Restore backup aborted, expected volume status ' '%(expected_status)s but got %(actual_status)s.') % {'expected_status': expected_status, 'actual_status': actual_status}) backup.status = 'available' backup.save() raise exception.InvalidVolume(reason=err) expected_status = 'restoring' actual_status = backup['status'] if actual_status != expected_status: err = (_('Restore backup aborted: expected backup status ' '%(expected_status)s but got %(actual_status)s.') % {'expected_status': expected_status, 'actual_status': actual_status}) self._update_backup_error(backup, context, err) self.db.volume_update(context, volume_id, {'status': 'error'}) raise exception.InvalidBackup(reason=err) if volume['size'] > backup['size']: LOG.info(_LI('Volume: %(vol_id)s, size: %(vol_size)d is ' 'larger than backup: %(backup_id)s, ' 'size: %(backup_size)d, continuing with restore.'), {'vol_id': volume['id'], 'vol_size': volume['size'], 'backup_id': backup['id'], 'backup_size': backup['size']}) backup_service = self._map_service_to_driver(backup['service']) configured_service = self.driver_name if backup_service != configured_service: err = _('Restore backup aborted, the backup service currently' ' configured [%(configured_service)s] is not the' ' backup service that was used to create this' ' backup [%(backup_service)s].') % { 'configured_service': configured_service, 'backup_service': backup_service, } backup.status = 'available' backup.save() self.db.volume_update(context, volume_id, {'status': 'error'}) raise exception.InvalidBackup(reason=err) try: # NOTE(flaper87): Verify the driver is enabled # before going forward. The exception will be caught, # the volume status will be set back to available and # the backup status to 'error' utils.require_driver_initialized(self.driver) backup_service = self.service.get_backup_driver(context) self._get_driver(backend).restore_backup(context, backup, volume, backup_service) except Exception: with excutils.save_and_reraise_exception(): self.db.volume_update(context, volume_id, {'status': 'error_restoring'}) backup.status = 'available' backup.save() self.db.volume_update(context, volume_id, {'status': 'available'}) backup.status = 'available' backup.save() LOG.info(_LI('Restore backup finished, backup %(backup_id)s restored' ' to volume %(volume_id)s.'), {'backup_id': backup.id, 'volume_id': volume_id}) self._notify_about_backup_usage(context, backup, "restore.end") def delete_backup(self, context, backup): """Delete volume backup from configured backup service.""" LOG.info(_LI('Delete backup started, backup: %s.'), backup.id) try: # NOTE(flaper87): Verify the driver is enabled # before going forward. The exception will be caught # and the backup status updated. Fail early since there # are no other status to change but backup's utils.require_driver_initialized(self.driver) except exception.DriverNotInitialized as err: with excutils.save_and_reraise_exception(): self._update_backup_error(backup, context, six.text_type(err)) self._notify_about_backup_usage(context, backup, "delete.start") backup.host = self.host backup.save() expected_status = 'deleting' actual_status = backup.status if actual_status != expected_status: err = _('Delete_backup aborted, expected backup status ' '%(expected_status)s but got %(actual_status)s.') \ % {'expected_status': expected_status, 'actual_status': actual_status} self._update_backup_error(backup, context, err) raise exception.InvalidBackup(reason=err) backup_service = self._map_service_to_driver(backup['service']) if backup_service is not None: configured_service = self.driver_name if backup_service != configured_service: err = _('Delete backup aborted, the backup service currently' ' configured [%(configured_service)s] is not the' ' backup service that was used to create this' ' backup [%(backup_service)s].')\ % {'configured_service': configured_service, 'backup_service': backup_service} self._update_backup_error(backup, context, err) raise exception.InvalidBackup(reason=err) try: backup_service = self.service.get_backup_driver(context) backup_service.delete(backup) except Exception as err: with excutils.save_and_reraise_exception(): self._update_backup_error(backup, context, six.text_type(err)) # Get reservations try: reserve_opts = { 'backups': -1, 'backup_gigabytes': -backup.size, } reservations = QUOTAS.reserve(context, project_id=backup.project_id, **reserve_opts) except Exception: reservations = None LOG.exception(_LE("Failed to update usages deleting backup")) backup.destroy() # Commit the reservations if reservations: QUOTAS.commit(context, reservations, project_id=backup.project_id) LOG.info(_LI('Delete backup finished, backup %s deleted.'), backup.id) self._notify_about_backup_usage(context, backup, "delete.end") def _notify_about_backup_usage(self, context, backup, event_suffix, extra_usage_info=None): volume_utils.notify_about_backup_usage( context, backup, event_suffix, extra_usage_info=extra_usage_info, host=self.host) def export_record(self, context, backup): """Export all volume backup metadata details to allow clean import. Export backup metadata so it could be re-imported into the database without any prerequisite in the backup database. :param context: running context :param backup: backup object to export :returns: backup_record - a description of how to import the backup :returns: contains 'backup_url' - how to import the backup, and :returns: 'backup_service' describing the needed driver. :raises: InvalidBackup """ LOG.info(_LI('Export record started, backup: %s.'), backup.id) expected_status = 'available' actual_status = backup.status if actual_status != expected_status: err = (_('Export backup aborted, expected backup status ' '%(expected_status)s but got %(actual_status)s.') % {'expected_status': expected_status, 'actual_status': actual_status}) raise exception.InvalidBackup(reason=err) backup_record = {} backup_record['backup_service'] = backup.service backup_service = self._map_service_to_driver(backup.service) configured_service = self.driver_name if backup_service != configured_service: err = (_('Export record aborted, the backup service currently' ' configured [%(configured_service)s] is not the' ' backup service that was used to create this' ' backup [%(backup_service)s].') % {'configured_service': configured_service, 'backup_service': backup_service}) raise exception.InvalidBackup(reason=err) # Call driver to create backup description string try: utils.require_driver_initialized(self.driver) backup_service = self.service.get_backup_driver(context) backup_url = backup_service.export_record(backup) backup_record['backup_url'] = backup_url except Exception as err: msg = six.text_type(err) raise exception.InvalidBackup(reason=msg) LOG.info(_LI('Export record finished, backup %s exported.'), backup.id) return backup_record def import_record(self, context, backup, backup_service, backup_url, backup_hosts): """Import all volume backup metadata details to the backup db. :param context: running context :param backup: The new backup object for the import :param backup_service: The needed backup driver for import :param backup_url: An identifier string to locate the backup :param backup_hosts: Potential hosts to execute the import :raises: InvalidBackup :raises: ServiceNotFound """ LOG.info(_LI('Import record started, backup_url: %s.'), backup_url) # Can we import this backup? if (backup_service != self.driver_name): # No, are there additional potential backup hosts in the list? if len(backup_hosts) > 0: # try the next host on the list, maybe he can import first_host = backup_hosts.pop() self.backup_rpcapi.import_record(context, first_host, backup, backup_service, backup_url, backup_hosts) else: # empty list - we are the last host on the list, fail err = _('Import record failed, cannot find backup ' 'service to perform the import. Request service ' '%(service)s') % {'service': backup_service} self._update_backup_error(backup, context, err) raise exception.ServiceNotFound(service_id=backup_service) else: # Yes... try: utils.require_driver_initialized(self.driver) backup_service = self.service.get_backup_driver(context) backup_options = backup_service.import_record(backup_url) except Exception as err: msg = six.text_type(err) self._update_backup_error(backup, context, msg) raise exception.InvalidBackup(reason=msg) required_import_options = ['display_name', 'display_description', 'container', 'size', 'service_metadata', 'service', 'object_count'] backup_update = {} backup_update['status'] = 'available' backup_update['service'] = self.driver_name backup_update['availability_zone'] = self.az backup_update['host'] = self.host for entry in required_import_options: if entry not in backup_options: msg = (_('Backup metadata received from driver for ' 'import is missing %s.'), entry) self._update_backup_error(backup, context, msg) raise exception.InvalidBackup(reason=msg) backup_update[entry] = backup_options[entry] # Update the database backup.update(backup_update) backup.save() # Verify backup try: if isinstance(backup_service, driver.BackupDriverWithVerify): backup_service.verify(backup.id) else: LOG.warning(_LW('Backup service %(service)s does not ' 'support verify. Backup id %(id)s is ' 'not verified. Skipping verify.'), {'service': self.driver_name, 'id': backup.id}) except exception.InvalidBackup as err: with excutils.save_and_reraise_exception(): self._update_backup_error(backup, context, six.text_type(err)) LOG.info(_LI('Import record id %s metadata from driver ' 'finished.'), backup.id) def reset_status(self, context, backup, status): """Reset volume backup status. :param context: running context :param backup: The backup object for reset status operation :param status: The status to be set :raises: InvalidBackup :raises: BackupVerifyUnsupportedDriver :raises: AttributeError """ LOG.info(_LI('Reset backup status started, backup_id: ' '%(backup_id)s, status: %(status)s.'), {'backup_id': backup.id, 'status': status}) try: # NOTE(flaper87): Verify the driver is enabled # before going forward. The exception will be caught # and the backup status updated. Fail early since there # are no other status to change but backup's utils.require_driver_initialized(self.driver) except exception.DriverNotInitialized: with excutils.save_and_reraise_exception(): LOG.exception(_LE("Backup driver has not been initialized")) backup_service = self._map_service_to_driver(backup.service) LOG.info(_LI('Backup service: %s.'), backup_service) if backup_service is not None: configured_service = self.driver_name if backup_service != configured_service: err = _('Reset backup status aborted, the backup service' ' currently configured [%(configured_service)s] ' 'is not the backup service that was used to create' ' this backup [%(backup_service)s].') % \ {'configured_service': configured_service, 'backup_service': backup_service} raise exception.InvalidBackup(reason=err) # Verify backup try: # check whether the backup is ok or not if status == 'available' and backup['status'] != 'restoring': # check whether we could verify the backup is ok or not if isinstance(backup_service, driver.BackupDriverWithVerify): backup_service.verify(backup.id) backup.status = status backup.save() # driver does not support verify function else: msg = (_('Backup service %(configured_service)s ' 'does not support verify. Backup id' ' %(id)s is not verified. ' 'Skipping verify.') % {'configured_service': self.driver_name, 'id': backup.id}) raise exception.BackupVerifyUnsupportedDriver( reason=msg) # reset status to error or from restoring to available else: if (status == 'error' or (status == 'available' and backup.status == 'restoring')): backup.status = status backup.save() except exception.InvalidBackup: with excutils.save_and_reraise_exception(): LOG.error(_LE("Backup id %s is not invalid. " "Skipping reset."), backup.id) except exception.BackupVerifyUnsupportedDriver: with excutils.save_and_reraise_exception(): LOG.error(_LE('Backup service %(configured_service)s ' 'does not support verify. Backup id ' '%(id)s is not verified. ' 'Skipping verify.'), {'configured_service': self.driver_name, 'id': backup.id}) except AttributeError: msg = (_('Backup service %(service)s does not support ' 'verify. Backup id %(id)s is not verified. ' 'Skipping reset.') % {'service': self.driver_name, 'id': backup.id}) LOG.error(msg) raise exception.BackupVerifyUnsupportedDriver( reason=msg) # send notification to ceilometer notifier_info = {'id': backup.id, 'update': {'status': status}} notifier = rpc.get_notifier('backupStatusUpdate') notifier.info(context, "backups.reset_status.end", notifier_info) def check_support_to_force_delete(self, context): """Check if the backup driver supports force delete operation. :param context: running context """ backup_service = self.service.get_backup_driver(context) return backup_service.support_force_delete