1d6235cc3f
Add new admin API for backup-export and backup-import. The new commands export the backup details (not actual backup) to a string that can be imported again in another OpenStack cloud or if the backup database was corrupted. The code includes a default backup driver implementation. Backup test code converted to use mock. blueprint cinder-backup-recover-api DocImpact new admin API calls backup-import and backup-export Change-Id: I564194929962e75c67630e73d8711ee6587706d4
301 lines
11 KiB
Python
301 lines
11 KiB
Python
# Copyright (C) 2013 Deutsche Telekom AG
|
|
# 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.
|
|
|
|
"""Base class for all backup drivers."""
|
|
|
|
from cinder.db import base
|
|
from cinder import exception
|
|
from cinder.openstack.common import jsonutils
|
|
from cinder.openstack.common import log as logging
|
|
from oslo.config import cfg
|
|
|
|
service_opts = [
|
|
cfg.IntOpt('backup_metadata_version', default=1,
|
|
help='Backup metadata version to be used when backing up '
|
|
'volume metadata. If this number is bumped, make sure the '
|
|
'service doing the restore supports the new version.')
|
|
]
|
|
|
|
CONF = cfg.CONF
|
|
CONF.register_opts(service_opts)
|
|
|
|
LOG = logging.getLogger(__name__)
|
|
|
|
|
|
class BackupMetadataAPI(base.Base):
|
|
|
|
TYPE_TAG_VOL_BASE_META = 'volume-base-metadata'
|
|
TYPE_TAG_VOL_META = 'volume-metadata'
|
|
TYPE_TAG_VOL_GLANCE_META = 'volume-glance-metadata'
|
|
|
|
def __init__(self, context, db_driver=None):
|
|
super(BackupMetadataAPI, self).__init__(db_driver)
|
|
self.context = context
|
|
|
|
@staticmethod
|
|
def _is_serializable(value):
|
|
"""Returns True if value is serializable."""
|
|
try:
|
|
jsonutils.dumps(value)
|
|
except TypeError:
|
|
LOG.info(_("Value with type=%s is not serializable") %
|
|
type(value))
|
|
return False
|
|
|
|
return True
|
|
|
|
def _save_vol_base_meta(self, container, volume_id):
|
|
"""Save base volume metadata to container.
|
|
|
|
This will fetch all fields from the db Volume object for volume_id and
|
|
save them in the provided container dictionary.
|
|
"""
|
|
type_tag = self.TYPE_TAG_VOL_BASE_META
|
|
LOG.debug(_("Getting metadata type '%s'") % type_tag)
|
|
meta = self.db.volume_get(self.context, volume_id)
|
|
if meta:
|
|
container[type_tag] = {}
|
|
for key, value in meta:
|
|
# Exclude fields that are "not JSON serializable"
|
|
if not self._is_serializable(value):
|
|
LOG.info(_("Unable to serialize field '%s' - excluding "
|
|
"from backup") % (key))
|
|
continue
|
|
container[type_tag][key] = value
|
|
|
|
LOG.debug(_("Completed fetching metadata type '%s'") % type_tag)
|
|
else:
|
|
LOG.debug(_("No metadata type '%s' available") % type_tag)
|
|
|
|
def _save_vol_meta(self, container, volume_id):
|
|
"""Save volume metadata to container.
|
|
|
|
This will fetch all fields from the db VolumeMetadata object for
|
|
volume_id and save them in the provided container dictionary.
|
|
"""
|
|
type_tag = self.TYPE_TAG_VOL_META
|
|
LOG.debug(_("Getting metadata type '%s'") % type_tag)
|
|
meta = self.db.volume_metadata_get(self.context, volume_id)
|
|
if meta:
|
|
container[type_tag] = {}
|
|
for entry in meta:
|
|
# Exclude fields that are "not JSON serializable"
|
|
if not self._is_serializable(meta[entry]):
|
|
LOG.info(_("Unable to serialize field '%s' - excluding "
|
|
"from backup") % (entry))
|
|
continue
|
|
container[type_tag][entry] = meta[entry]
|
|
|
|
LOG.debug(_("Completed fetching metadata type '%s'") % type_tag)
|
|
else:
|
|
LOG.debug(_("No metadata type '%s' available") % type_tag)
|
|
|
|
def _save_vol_glance_meta(self, container, volume_id):
|
|
"""Save volume Glance metadata to container.
|
|
|
|
This will fetch all fields from the db VolumeGlanceMetadata object for
|
|
volume_id and save them in the provided container dictionary.
|
|
"""
|
|
type_tag = self.TYPE_TAG_VOL_GLANCE_META
|
|
LOG.debug(_("Getting metadata type '%s'") % type_tag)
|
|
try:
|
|
meta = self.db.volume_glance_metadata_get(self.context, volume_id)
|
|
if meta:
|
|
container[type_tag] = {}
|
|
for entry in meta:
|
|
# Exclude fields that are "not JSON serializable"
|
|
if not self._is_serializable(entry.value):
|
|
LOG.info(_("Unable to serialize field '%s' - "
|
|
"excluding from backup") % (entry))
|
|
continue
|
|
container[type_tag][entry.key] = entry.value
|
|
|
|
LOG.debug(_("Completed fetching metadata type '%s'") % type_tag)
|
|
except exception.GlanceMetadataNotFound:
|
|
LOG.debug(_("No metadata type '%s' available") % type_tag)
|
|
|
|
@staticmethod
|
|
def _filter(metadata, fields):
|
|
"""Returns set of metadata restricted to required fields.
|
|
|
|
If fields is empty list, the full set is returned.
|
|
"""
|
|
if fields == []:
|
|
return metadata
|
|
|
|
subset = {}
|
|
for field in fields:
|
|
if field in metadata:
|
|
subset[field] = metadata[field]
|
|
else:
|
|
LOG.debug(_("Excluding field '%s'") % (field))
|
|
|
|
return subset
|
|
|
|
def _restore_vol_base_meta(self, metadata, volume_id, fields):
|
|
"""Restore values to Volume object for provided fields."""
|
|
LOG.debug(_("Restoring volume base metadata"))
|
|
# Only set the display_name if it was not None since the
|
|
# restore action will have set a name which is more useful than
|
|
# None.
|
|
key = 'display_name'
|
|
if key in fields and key in metadata and metadata[key] is None:
|
|
fields = [f for f in fields if f != key]
|
|
|
|
metadata = self._filter(metadata, fields)
|
|
self.db.volume_update(self.context, volume_id, metadata)
|
|
|
|
def _restore_vol_meta(self, metadata, volume_id, fields):
|
|
"""Restore values to VolumeMetadata object for provided fields."""
|
|
LOG.debug(_("Restoring volume metadata"))
|
|
metadata = self._filter(metadata, fields)
|
|
self.db.volume_metadata_update(self.context, volume_id, metadata, True)
|
|
|
|
def _restore_vol_glance_meta(self, metadata, volume_id, fields):
|
|
"""Restore values to VolumeGlanceMetadata object for provided fields.
|
|
|
|
First delete any existing metadata then save new values.
|
|
"""
|
|
LOG.debug(_("Restoring volume glance metadata"))
|
|
metadata = self._filter(metadata, fields)
|
|
self.db.volume_glance_metadata_delete_by_volume(self.context,
|
|
volume_id)
|
|
for key, value in metadata.items():
|
|
self.db.volume_glance_metadata_create(self.context,
|
|
volume_id,
|
|
key, value)
|
|
|
|
# Now mark the volume as bootable
|
|
self.db.volume_update(self.context, volume_id,
|
|
{'bootable': True})
|
|
|
|
def _v1_restore_factory(self):
|
|
"""All metadata is backed up but we selectively restore.
|
|
|
|
Returns a dictionary of the form:
|
|
|
|
{<type tag>: (<fields list>, <restore function>)}
|
|
|
|
Empty field list indicates that all backed up fields should be
|
|
restored.
|
|
"""
|
|
return {self.TYPE_TAG_VOL_BASE_META:
|
|
(self._restore_vol_base_meta,
|
|
['display_name', 'display_description']),
|
|
self.TYPE_TAG_VOL_META:
|
|
(self._restore_vol_meta, []),
|
|
self.TYPE_TAG_VOL_GLANCE_META:
|
|
(self._restore_vol_glance_meta, [])}
|
|
|
|
def get(self, volume_id):
|
|
"""Get volume metadata.
|
|
|
|
Returns a json-encoded dict containing all metadata and the restore
|
|
version i.e. the version used to decide what actually gets restored
|
|
from this container when doing a backup restore.
|
|
"""
|
|
container = {'version': CONF.backup_metadata_version}
|
|
self._save_vol_base_meta(container, volume_id)
|
|
self._save_vol_meta(container, volume_id)
|
|
self._save_vol_glance_meta(container, volume_id)
|
|
|
|
if container:
|
|
return jsonutils.dumps(container)
|
|
else:
|
|
return None
|
|
|
|
def put(self, volume_id, json_metadata):
|
|
"""Restore volume metadata to a volume.
|
|
|
|
The json container should contain a version that is supported here.
|
|
"""
|
|
meta_container = jsonutils.loads(json_metadata)
|
|
version = meta_container['version']
|
|
if version == 1:
|
|
factory = self._v1_restore_factory()
|
|
else:
|
|
msg = (_("Unsupported backup metadata version (%s)") % (version))
|
|
raise exception.BackupMetadataUnsupportedVersion(msg)
|
|
|
|
for type in factory:
|
|
func = factory[type][0]
|
|
fields = factory[type][1]
|
|
if type in meta_container:
|
|
func(meta_container[type], volume_id, fields)
|
|
else:
|
|
msg = _("No metadata of type '%s' to restore") % (type)
|
|
LOG.debug(msg)
|
|
|
|
|
|
class BackupDriver(base.Base):
|
|
|
|
def __init__(self, context, db_driver=None):
|
|
super(BackupDriver, self).__init__(db_driver)
|
|
self.context = context
|
|
self.backup_meta_api = BackupMetadataAPI(context, db_driver)
|
|
|
|
def get_metadata(self, volume_id):
|
|
return self.backup_meta_api.get(volume_id)
|
|
|
|
def put_metadata(self, volume_id, json_metadata):
|
|
self.backup_meta_api.put(volume_id, json_metadata)
|
|
|
|
def backup(self, backup, volume_file, backup_metadata=False):
|
|
"""Start a backup of a specified volume."""
|
|
raise NotImplementedError()
|
|
|
|
def restore(self, backup, volume_id, volume_file):
|
|
"""Restore a saved backup."""
|
|
raise NotImplementedError()
|
|
|
|
def delete(self, backup):
|
|
"""Delete a saved backup."""
|
|
raise NotImplementedError()
|
|
|
|
def export_record(self, backup):
|
|
"""Export backup record.
|
|
|
|
Default backup driver implementation.
|
|
Serialize the backup record describing the backup into a string.
|
|
|
|
:param backup: backup entry to export
|
|
:returns backup_url - a string describing the backup record
|
|
"""
|
|
retval = jsonutils.dumps(backup)
|
|
return retval.encode("base64")
|
|
|
|
def import_record(self, backup_url):
|
|
"""Import and verify backup record.
|
|
|
|
Default backup driver implementation.
|
|
De-serialize the backup record into a dictionary, so we can
|
|
update the database.
|
|
|
|
:param backup_url: driver specific backup record string
|
|
:returns dictionary object with database updates
|
|
"""
|
|
return jsonutils.loads(backup_url.decode("base64"))
|
|
|
|
def verify(self, backup):
|
|
"""Verify that the backup exists on the backend.
|
|
|
|
Verify that the backup is OK, possibly following an import record
|
|
operation.
|
|
|
|
:param backup: backup id of the backup to verify
|
|
:raises: InvalidBackup, NotImplementedError
|
|
"""
|
|
raise NotImplementedError()
|