Merge "Support metadata for backup resource"
This commit is contained in:
commit
48630526e2
@ -134,6 +134,7 @@ class BackupsController(wsgi.Controller):
|
|||||||
|
|
||||||
context = req.environ['cinder.context']
|
context = req.environ['cinder.context']
|
||||||
backup = body['backup']
|
backup = body['backup']
|
||||||
|
req_version = req.api_version_request
|
||||||
|
|
||||||
try:
|
try:
|
||||||
volume_id = backup['volume_id']
|
volume_id = backup['volume_id']
|
||||||
@ -150,6 +151,8 @@ class BackupsController(wsgi.Controller):
|
|||||||
incremental = backup.get('incremental', False)
|
incremental = backup.get('incremental', False)
|
||||||
force = backup.get('force', False)
|
force = backup.get('force', False)
|
||||||
snapshot_id = backup.get('snapshot_id', None)
|
snapshot_id = backup.get('snapshot_id', None)
|
||||||
|
metadata = backup.get(
|
||||||
|
'metadata', None) if req_version.matches("3.43") else None
|
||||||
LOG.info("Creating backup of volume %(volume_id)s in container"
|
LOG.info("Creating backup of volume %(volume_id)s in container"
|
||||||
" %(container)s",
|
" %(container)s",
|
||||||
{'volume_id': volume_id, 'container': container},
|
{'volume_id': volume_id, 'container': container},
|
||||||
@ -159,9 +162,11 @@ class BackupsController(wsgi.Controller):
|
|||||||
new_backup = self.backup_api.create(context, name, description,
|
new_backup = self.backup_api.create(context, name, description,
|
||||||
volume_id, container,
|
volume_id, container,
|
||||||
incremental, None, force,
|
incremental, None, force,
|
||||||
snapshot_id)
|
snapshot_id, metadata)
|
||||||
except (exception.InvalidVolume,
|
except (exception.InvalidVolume,
|
||||||
exception.InvalidSnapshot) as error:
|
exception.InvalidSnapshot,
|
||||||
|
exception.InvalidVolumeMetadata,
|
||||||
|
exception.InvalidVolumeMetadataSize) as error:
|
||||||
raise exc.HTTPBadRequest(explanation=error.msg)
|
raise exc.HTTPBadRequest(explanation=error.msg)
|
||||||
# Other not found exceptions will be handled at the wsgi level
|
# Other not found exceptions will be handled at the wsgi level
|
||||||
except exception.ServiceNotFound as error:
|
except exception.ServiceNotFound as error:
|
||||||
|
@ -105,6 +105,7 @@ REST_API_VERSION_HISTORY = """
|
|||||||
Administrator can disable this ability by updating the
|
Administrator can disable this ability by updating the
|
||||||
'volume:extend_attached_volume' policy rule. Extend in reserved
|
'volume:extend_attached_volume' policy rule. Extend in reserved
|
||||||
state is intentionally NOT allowed.
|
state is intentionally NOT allowed.
|
||||||
|
* 3.43 - Support backup CRUD with metadata.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# The minimum and maximum versions of the API supported
|
# The minimum and maximum versions of the API supported
|
||||||
@ -112,7 +113,7 @@ REST_API_VERSION_HISTORY = """
|
|||||||
# minimum version of the API supported.
|
# minimum version of the API supported.
|
||||||
# Explicitly using /v1 or /v2 endpoints will still work
|
# Explicitly using /v1 or /v2 endpoints will still work
|
||||||
_MIN_API_VERSION = "3.0"
|
_MIN_API_VERSION = "3.0"
|
||||||
_MAX_API_VERSION = "3.42"
|
_MAX_API_VERSION = "3.43"
|
||||||
_LEGACY_API_VERSION1 = "1.0"
|
_LEGACY_API_VERSION1 = "1.0"
|
||||||
_LEGACY_API_VERSION2 = "2.0"
|
_LEGACY_API_VERSION2 = "2.0"
|
||||||
|
|
||||||
|
@ -361,3 +361,7 @@ user documentation.
|
|||||||
Administrator can disable this ability by updating the
|
Administrator can disable this ability by updating the
|
||||||
``volume:extend_attached_volume`` policy rule. Extend of a resered
|
``volume:extend_attached_volume`` policy rule. Extend of a resered
|
||||||
Volume is NOT allowed.
|
Volume is NOT allowed.
|
||||||
|
|
||||||
|
3.43
|
||||||
|
----
|
||||||
|
Support backup CRUD with metadata.
|
||||||
|
@ -39,6 +39,7 @@ class BackupsController(backups_v2.BackupsController):
|
|||||||
"""Update a backup."""
|
"""Update a backup."""
|
||||||
context = req.environ['cinder.context']
|
context = req.environ['cinder.context']
|
||||||
self.assert_valid_body(body, 'backup')
|
self.assert_valid_body(body, 'backup')
|
||||||
|
req_version = req.api_version_request
|
||||||
|
|
||||||
backup_update = body['backup']
|
backup_update = body['backup']
|
||||||
|
|
||||||
@ -49,6 +50,8 @@ class BackupsController(backups_v2.BackupsController):
|
|||||||
if 'description' in backup_update:
|
if 'description' in backup_update:
|
||||||
update_dict['display_description'] = (
|
update_dict['display_description'] = (
|
||||||
backup_update.pop('description'))
|
backup_update.pop('description'))
|
||||||
|
if req_version.matches('3.43') and 'metadata' in backup_update:
|
||||||
|
update_dict['metadata'] = backup_update.pop('metadata')
|
||||||
# Check no unsupported fields.
|
# Check no unsupported fields.
|
||||||
if backup_update:
|
if backup_update:
|
||||||
msg = _("Unsupported fields %s.") % (", ".join(backup_update))
|
msg = _("Unsupported fields %s.") % (", ".join(backup_update))
|
||||||
|
@ -56,7 +56,7 @@ class ViewBuilder(common.ViewBuilder):
|
|||||||
|
|
||||||
def detail(self, request, backup):
|
def detail(self, request, backup):
|
||||||
"""Detailed view of a single backup."""
|
"""Detailed view of a single backup."""
|
||||||
return {
|
backup_dict = {
|
||||||
'backup': {
|
'backup': {
|
||||||
'id': backup.get('id'),
|
'id': backup.get('id'),
|
||||||
'status': backup.get('status'),
|
'status': backup.get('status'),
|
||||||
@ -77,6 +77,10 @@ class ViewBuilder(common.ViewBuilder):
|
|||||||
'data_timestamp': backup.data_timestamp,
|
'data_timestamp': backup.data_timestamp,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
req_version = request.api_version_request
|
||||||
|
if req_version.matches("3.43"):
|
||||||
|
backup_dict['backup']['metadata'] = backup.metadata
|
||||||
|
return backup_dict
|
||||||
|
|
||||||
def _list_view(self, func, request, backups, backup_count):
|
def _list_view(self, func, request, backups, backup_count):
|
||||||
"""Provide a view for a list of backups."""
|
"""Provide a view for a list of backups."""
|
||||||
|
@ -39,6 +39,7 @@ from cinder.objects import fields
|
|||||||
import cinder.policy
|
import cinder.policy
|
||||||
from cinder import quota
|
from cinder import quota
|
||||||
from cinder import quota_utils
|
from cinder import quota_utils
|
||||||
|
from cinder import utils
|
||||||
import cinder.volume
|
import cinder.volume
|
||||||
from cinder.volume import utils as volume_utils
|
from cinder.volume import utils as volume_utils
|
||||||
|
|
||||||
@ -201,9 +202,10 @@ class API(base.Base):
|
|||||||
|
|
||||||
def create(self, context, name, description, volume_id,
|
def create(self, context, name, description, volume_id,
|
||||||
container, incremental=False, availability_zone=None,
|
container, incremental=False, availability_zone=None,
|
||||||
force=False, snapshot_id=None):
|
force=False, snapshot_id=None, metadata=None):
|
||||||
"""Make the RPC call to create a volume backup."""
|
"""Make the RPC call to create a volume backup."""
|
||||||
check_policy(context, 'create')
|
check_policy(context, 'create')
|
||||||
|
utils.check_metadata_properties(metadata)
|
||||||
volume = self.volume_api.get(context, volume_id)
|
volume = self.volume_api.get(context, volume_id)
|
||||||
snapshot = None
|
snapshot = None
|
||||||
if snapshot_id:
|
if snapshot_id:
|
||||||
@ -314,6 +316,7 @@ class API(base.Base):
|
|||||||
'host': host,
|
'host': host,
|
||||||
'snapshot_id': snapshot_id,
|
'snapshot_id': snapshot_id,
|
||||||
'data_timestamp': data_timestamp,
|
'data_timestamp': data_timestamp,
|
||||||
|
'metadata': metadata or {}
|
||||||
}
|
}
|
||||||
backup = objects.Backup(context=context, **kwargs)
|
backup = objects.Backup(context=context, **kwargs)
|
||||||
backup.create()
|
backup.create()
|
||||||
@ -495,6 +498,7 @@ class API(base.Base):
|
|||||||
'project_id': context.project_id,
|
'project_id': context.project_id,
|
||||||
'volume_id': IMPORT_VOLUME_ID,
|
'volume_id': IMPORT_VOLUME_ID,
|
||||||
'status': fields.BackupStatus.CREATING,
|
'status': fields.BackupStatus.CREATING,
|
||||||
|
'metadata': {}
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -1183,6 +1183,14 @@ def backup_create(context, values):
|
|||||||
return IMPL.backup_create(context, values)
|
return IMPL.backup_create(context, values)
|
||||||
|
|
||||||
|
|
||||||
|
def backup_metadata_get(context, backup_id):
|
||||||
|
return IMPL.backup_metadata_get(context, backup_id)
|
||||||
|
|
||||||
|
|
||||||
|
def backup_metadata_update(context, backup_id, metadata, delete):
|
||||||
|
return IMPL.backup_metadata_update(context, backup_id, metadata, delete)
|
||||||
|
|
||||||
|
|
||||||
def backup_get_all_by_project(context, project_id, filters=None, marker=None,
|
def backup_get_all_by_project(context, project_id, filters=None, marker=None,
|
||||||
limit=None, offset=None, sort_keys=None,
|
limit=None, offset=None, sort_keys=None,
|
||||||
sort_dirs=None):
|
sort_dirs=None):
|
||||||
|
@ -226,6 +226,21 @@ def require_snapshot_exists(f):
|
|||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
|
def require_backup_exists(f):
|
||||||
|
"""Decorator to require the specified snapshot to exist.
|
||||||
|
|
||||||
|
Requires the wrapped function to use context and backup_id as
|
||||||
|
their first two arguments.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@functools.wraps(f)
|
||||||
|
def wrapper(context, backup_id, *args, **kwargs):
|
||||||
|
if not resource_exists(context, models.Backup, backup_id):
|
||||||
|
raise exception.BackupNotFound(backup_id=backup_id)
|
||||||
|
return f(context, backup_id, *args, **kwargs)
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
def _retry_on_deadlock(f):
|
def _retry_on_deadlock(f):
|
||||||
"""Decorator to retry a DB API call if Deadlock was received."""
|
"""Decorator to retry a DB API call if Deadlock was received."""
|
||||||
@functools.wraps(f)
|
@functools.wraps(f)
|
||||||
@ -4954,11 +4969,10 @@ def backup_get(context, backup_id, read_deleted=None, project_only=True):
|
|||||||
|
|
||||||
def _backup_get(context, backup_id, session=None, read_deleted=None,
|
def _backup_get(context, backup_id, session=None, read_deleted=None,
|
||||||
project_only=True):
|
project_only=True):
|
||||||
result = model_query(context, models.Backup, session=session,
|
result = model_query(
|
||||||
project_only=project_only,
|
context, models.Backup, session=session, project_only=project_only,
|
||||||
read_deleted=read_deleted).\
|
read_deleted=read_deleted).options(
|
||||||
filter_by(id=backup_id).\
|
joinedload('backup_metadata')).filter_by(id=backup_id).first()
|
||||||
first()
|
|
||||||
|
|
||||||
if not result:
|
if not result:
|
||||||
raise exception.BackupNotFound(backup_id=backup_id)
|
raise exception.BackupNotFound(backup_id=backup_id)
|
||||||
@ -4983,8 +4997,9 @@ def _backup_get_all(context, filters=None, marker=None, limit=None,
|
|||||||
|
|
||||||
|
|
||||||
def _backups_get_query(context, session=None, project_only=False):
|
def _backups_get_query(context, session=None, project_only=False):
|
||||||
return model_query(context, models.Backup, session=session,
|
return model_query(
|
||||||
project_only=project_only)
|
context, models.Backup, session=session,
|
||||||
|
project_only=project_only).options(joinedload('backup_metadata'))
|
||||||
|
|
||||||
|
|
||||||
@apply_like_filters(model=models.Backup)
|
@apply_like_filters(model=models.Backup)
|
||||||
@ -4993,7 +5008,18 @@ def _process_backups_filters(query, filters):
|
|||||||
# Ensure that filters' keys exist on the model
|
# Ensure that filters' keys exist on the model
|
||||||
if not is_valid_model_filters(models.Backup, filters):
|
if not is_valid_model_filters(models.Backup, filters):
|
||||||
return
|
return
|
||||||
query = query.filter_by(**filters)
|
filters_dict = {}
|
||||||
|
for key, value in filters.items():
|
||||||
|
if key == 'metadata':
|
||||||
|
col_attr = getattr(models.Snapshot, 'snapshot_metadata')
|
||||||
|
for k, v in value.items():
|
||||||
|
query = query.filter(col_attr.any(key=k, value=v))
|
||||||
|
else:
|
||||||
|
filters_dict[key] = value
|
||||||
|
|
||||||
|
# Apply exact matches
|
||||||
|
if filters_dict:
|
||||||
|
query = query.filter_by(**filters_dict)
|
||||||
return query
|
return query
|
||||||
|
|
||||||
|
|
||||||
@ -5006,7 +5032,9 @@ def backup_get_all(context, filters=None, marker=None, limit=None,
|
|||||||
|
|
||||||
@require_admin_context
|
@require_admin_context
|
||||||
def backup_get_all_by_host(context, host):
|
def backup_get_all_by_host(context, host):
|
||||||
return model_query(context, models.Backup).filter_by(host=host).all()
|
return model_query(
|
||||||
|
context, models.Backup).options(
|
||||||
|
joinedload('backup_metadata')).filter_by(host=host).all()
|
||||||
|
|
||||||
|
|
||||||
@require_context
|
@require_context
|
||||||
@ -5044,7 +5072,8 @@ def backup_get_all_by_volume(context, volume_id, filters=None):
|
|||||||
def backup_get_all_active_by_window(context, begin, end=None, project_id=None):
|
def backup_get_all_active_by_window(context, begin, end=None, project_id=None):
|
||||||
"""Return backups that were active during window."""
|
"""Return backups that were active during window."""
|
||||||
|
|
||||||
query = model_query(context, models.Backup, read_deleted="yes")
|
query = model_query(context, models.Backup, read_deleted="yes").options(
|
||||||
|
joinedload('backup_metadata'))
|
||||||
query = query.filter(or_(models.Backup.deleted_at == None, # noqa
|
query = query.filter(or_(models.Backup.deleted_at == None, # noqa
|
||||||
models.Backup.deleted_at > begin))
|
models.Backup.deleted_at > begin))
|
||||||
if end:
|
if end:
|
||||||
@ -5058,15 +5087,18 @@ def backup_get_all_active_by_window(context, begin, end=None, project_id=None):
|
|||||||
@handle_db_data_error
|
@handle_db_data_error
|
||||||
@require_context
|
@require_context
|
||||||
def backup_create(context, values):
|
def backup_create(context, values):
|
||||||
backup = models.Backup()
|
values['backup_metadata'] = _metadata_refs(values.get('metadata'),
|
||||||
|
models.BackupMetadata)
|
||||||
if not values.get('id'):
|
if not values.get('id'):
|
||||||
values['id'] = str(uuid.uuid4())
|
values['id'] = str(uuid.uuid4())
|
||||||
backup.update(values)
|
|
||||||
|
|
||||||
session = get_session()
|
session = get_session()
|
||||||
with session.begin():
|
with session.begin():
|
||||||
backup.save(session)
|
backup_ref = models.Backup()
|
||||||
return backup
|
backup_ref.update(values)
|
||||||
|
session.add(backup_ref)
|
||||||
|
|
||||||
|
return _backup_get(context, values['id'], session=session)
|
||||||
|
|
||||||
|
|
||||||
@handle_db_data_error
|
@handle_db_data_error
|
||||||
@ -5083,17 +5115,99 @@ def backup_update(context, backup_id, values):
|
|||||||
|
|
||||||
@require_admin_context
|
@require_admin_context
|
||||||
def backup_destroy(context, backup_id):
|
def backup_destroy(context, backup_id):
|
||||||
|
utcnow = timeutils.utcnow()
|
||||||
updated_values = {'status': fields.BackupStatus.DELETED,
|
updated_values = {'status': fields.BackupStatus.DELETED,
|
||||||
'deleted': True,
|
'deleted': True,
|
||||||
'deleted_at': timeutils.utcnow(),
|
'deleted_at': utcnow,
|
||||||
'updated_at': literal_column('updated_at')}
|
'updated_at': literal_column('updated_at')}
|
||||||
model_query(context, models.Backup).\
|
session = get_session()
|
||||||
filter_by(id=backup_id).\
|
with session.begin():
|
||||||
update(updated_values)
|
model_query(context, models.Backup, session=session).\
|
||||||
|
filter_by(id=backup_id).\
|
||||||
|
update(updated_values)
|
||||||
|
model_query(context, models.BackupMetadata, session=session).\
|
||||||
|
filter_by(backup_id=backup_id).\
|
||||||
|
update({'deleted': True,
|
||||||
|
'deleted_at': utcnow,
|
||||||
|
'updated_at': literal_column('updated_at')})
|
||||||
del updated_values['updated_at']
|
del updated_values['updated_at']
|
||||||
return updated_values
|
return updated_values
|
||||||
|
|
||||||
|
|
||||||
|
@require_context
|
||||||
|
@require_backup_exists
|
||||||
|
def backup_metadata_get(context, backup_id):
|
||||||
|
return _backup_metadata_get(context, backup_id)
|
||||||
|
|
||||||
|
|
||||||
|
@require_context
|
||||||
|
def _backup_metadata_get(context, backup_id, session=None):
|
||||||
|
rows = _backup_metadata_get_query(context, backup_id, session).all()
|
||||||
|
result = {}
|
||||||
|
for row in rows:
|
||||||
|
result[row['key']] = row['value']
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _backup_metadata_get_query(context, backup_id, session=None):
|
||||||
|
return model_query(
|
||||||
|
context, models.BackupMetadata,
|
||||||
|
session=session, read_deleted="no").filter_by(backup_id=backup_id)
|
||||||
|
|
||||||
|
|
||||||
|
@require_context
|
||||||
|
def _backup_metadata_get_item(context, backup_id, key, session=None):
|
||||||
|
result = _backup_metadata_get_query(
|
||||||
|
context, backup_id, session=session).filter_by(key=key).first()
|
||||||
|
|
||||||
|
if not result:
|
||||||
|
raise exception.BackupMetadataNotFound(metadata_key=key,
|
||||||
|
backup_id=backup_id)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@require_context
|
||||||
|
@require_backup_exists
|
||||||
|
@handle_db_data_error
|
||||||
|
@_retry_on_deadlock
|
||||||
|
def backup_metadata_update(context, backup_id, metadata, delete):
|
||||||
|
session = get_session()
|
||||||
|
with session.begin():
|
||||||
|
# Set existing metadata to deleted if delete argument is True
|
||||||
|
if delete:
|
||||||
|
original_metadata = _backup_metadata_get(context, backup_id,
|
||||||
|
session)
|
||||||
|
for meta_key, meta_value in original_metadata.items():
|
||||||
|
if meta_key not in metadata:
|
||||||
|
meta_ref = _backup_metadata_get_item(context,
|
||||||
|
backup_id,
|
||||||
|
meta_key, session)
|
||||||
|
meta_ref.update({'deleted': True,
|
||||||
|
'deleted_at': timeutils.utcnow()})
|
||||||
|
meta_ref.save(session=session)
|
||||||
|
|
||||||
|
meta_ref = None
|
||||||
|
|
||||||
|
# Now update all existing items with new values, or create new meta
|
||||||
|
# objects
|
||||||
|
for meta_key, meta_value in metadata.items():
|
||||||
|
|
||||||
|
# update the value whether it exists or not
|
||||||
|
item = {"value": meta_value}
|
||||||
|
|
||||||
|
try:
|
||||||
|
meta_ref = _backup_metadata_get_item(context, backup_id,
|
||||||
|
meta_key, session)
|
||||||
|
except exception.BackupMetadataNotFound:
|
||||||
|
meta_ref = models.BackupMetadata()
|
||||||
|
item.update({"key": meta_key, "backup_id": backup_id})
|
||||||
|
|
||||||
|
meta_ref.update(item)
|
||||||
|
meta_ref.save(session=session)
|
||||||
|
|
||||||
|
return backup_metadata_get(context, backup_id)
|
||||||
|
|
||||||
###############################
|
###############################
|
||||||
|
|
||||||
|
|
||||||
@ -5868,6 +5982,11 @@ def is_valid_model_filters(model, filters, exclude_list=None):
|
|||||||
for key in filters.keys():
|
for key in filters.keys():
|
||||||
if exclude_list and key in exclude_list:
|
if exclude_list and key in exclude_list:
|
||||||
continue
|
continue
|
||||||
|
if key == 'metadata':
|
||||||
|
if not isinstance(filters[key], dict):
|
||||||
|
LOG.debug("Metadata filter value is not valid dictionary")
|
||||||
|
return False
|
||||||
|
continue
|
||||||
try:
|
try:
|
||||||
key = key.rstrip('~')
|
key = key.rstrip('~')
|
||||||
getattr(model, key)
|
getattr(model, key)
|
||||||
|
@ -0,0 +1,50 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
from oslo_db.sqlalchemy import utils
|
||||||
|
from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer
|
||||||
|
from sqlalchemy import MetaData, String, Table
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade(migrate_engine):
|
||||||
|
"""Add backup_metadata table."""
|
||||||
|
|
||||||
|
meta = MetaData()
|
||||||
|
meta.bind = migrate_engine
|
||||||
|
|
||||||
|
Table('backups', meta, autoload=True)
|
||||||
|
|
||||||
|
backup_metadata = Table(
|
||||||
|
'backup_metadata', meta,
|
||||||
|
Column('created_at', DateTime(timezone=False)),
|
||||||
|
Column('updated_at', DateTime(timezone=False)),
|
||||||
|
Column('deleted_at', DateTime(timezone=False)),
|
||||||
|
Column('deleted', Boolean(), default=False),
|
||||||
|
Column('id', Integer, primary_key=True, nullable=False),
|
||||||
|
Column('backup_id', String(36),
|
||||||
|
ForeignKey('backups.id'),
|
||||||
|
nullable=False),
|
||||||
|
Column('key', String(255)),
|
||||||
|
Column('value', String(255)),
|
||||||
|
mysql_engine='InnoDB',
|
||||||
|
mysql_charset='utf8'
|
||||||
|
)
|
||||||
|
|
||||||
|
backup_metadata.create()
|
||||||
|
|
||||||
|
if not utils.index_exists_on_columns(migrate_engine,
|
||||||
|
'backup_metadata',
|
||||||
|
['backup_id']):
|
||||||
|
utils.add_index(migrate_engine,
|
||||||
|
'backup_metadata',
|
||||||
|
'backup_metadata_backup_id_idx',
|
||||||
|
['backup_id'])
|
@ -756,6 +756,20 @@ class Backup(BASE, CinderBase):
|
|||||||
return fail_reason and fail_reason[:255] or ''
|
return fail_reason and fail_reason[:255] or ''
|
||||||
|
|
||||||
|
|
||||||
|
class BackupMetadata(BASE, CinderBase):
|
||||||
|
"""Represents a metadata key/value pair for a backup."""
|
||||||
|
__tablename__ = 'backup_metadata'
|
||||||
|
id = Column(Integer, primary_key=True)
|
||||||
|
key = Column(String(255))
|
||||||
|
value = Column(String(255))
|
||||||
|
backup_id = Column(String(36), ForeignKey('backups.id'), nullable=False)
|
||||||
|
backup = relationship(Backup, backref="backup_metadata",
|
||||||
|
foreign_keys=backup_id,
|
||||||
|
primaryjoin='and_('
|
||||||
|
'BackupMetadata.backup_id == Backup.id,'
|
||||||
|
'BackupMetadata.deleted == False)')
|
||||||
|
|
||||||
|
|
||||||
class Encryption(BASE, CinderBase):
|
class Encryption(BASE, CinderBase):
|
||||||
"""Represents encryption requirement for a volume type.
|
"""Represents encryption requirement for a volume type.
|
||||||
|
|
||||||
|
@ -753,6 +753,11 @@ class BackupMetadataUnsupportedVersion(BackupDriverException):
|
|||||||
message = _("Unsupported backup metadata version requested")
|
message = _("Unsupported backup metadata version requested")
|
||||||
|
|
||||||
|
|
||||||
|
class BackupMetadataNotFound(NotFound):
|
||||||
|
message = _("Backup %(backup_id)s has no metadata with "
|
||||||
|
"key %(metadata_key)s.")
|
||||||
|
|
||||||
|
|
||||||
class BackupVerifyUnsupportedDriver(BackupDriverException):
|
class BackupVerifyUnsupportedDriver(BackupDriverException):
|
||||||
message = _("Unsupported backup verify driver")
|
message = _("Unsupported backup verify driver")
|
||||||
|
|
||||||
|
@ -38,7 +38,10 @@ class Backup(base.CinderPersistentObject, base.CinderObject,
|
|||||||
# Version 1.2: Add new field snapshot_id and data_timestamp.
|
# Version 1.2: Add new field snapshot_id and data_timestamp.
|
||||||
# Version 1.3: Changed 'status' field to use BackupStatusField
|
# Version 1.3: Changed 'status' field to use BackupStatusField
|
||||||
# Version 1.4: Add restore_volume_id
|
# Version 1.4: Add restore_volume_id
|
||||||
VERSION = '1.4'
|
# Version 1.5: Add metadata
|
||||||
|
VERSION = '1.5'
|
||||||
|
|
||||||
|
OPTIONAL_FIELDS = ('metadata',)
|
||||||
|
|
||||||
fields = {
|
fields = {
|
||||||
'id': fields.UUIDField(),
|
'id': fields.UUIDField(),
|
||||||
@ -71,10 +74,26 @@ class Backup(base.CinderPersistentObject, base.CinderObject,
|
|||||||
'snapshot_id': fields.StringField(nullable=True),
|
'snapshot_id': fields.StringField(nullable=True),
|
||||||
'data_timestamp': fields.DateTimeField(nullable=True),
|
'data_timestamp': fields.DateTimeField(nullable=True),
|
||||||
'restore_volume_id': fields.StringField(nullable=True),
|
'restore_volume_id': fields.StringField(nullable=True),
|
||||||
|
'metadata': fields.DictOfStringsField(nullable=True),
|
||||||
}
|
}
|
||||||
|
|
||||||
obj_extra_fields = ['name', 'is_incremental', 'has_dependent_backups']
|
obj_extra_fields = ['name', 'is_incremental', 'has_dependent_backups']
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super(Backup, self).__init__(*args, **kwargs)
|
||||||
|
self._orig_metadata = {}
|
||||||
|
|
||||||
|
self._reset_metadata_tracking()
|
||||||
|
|
||||||
|
def _reset_metadata_tracking(self, fields=None):
|
||||||
|
if fields is None or 'metadata' in fields:
|
||||||
|
self._orig_metadata = (dict(self.metadata)
|
||||||
|
if self.obj_attr_is_set('metadata') else {})
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _get_expected_attrs(cls, context, *args, **kwargs):
|
||||||
|
return 'metadata',
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
return CONF.backup_name_template % self.id
|
return CONF.backup_name_template % self.id
|
||||||
@ -92,18 +111,50 @@ class Backup(base.CinderPersistentObject, base.CinderObject,
|
|||||||
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)
|
||||||
|
|
||||||
@staticmethod
|
@classmethod
|
||||||
def _from_db_object(context, backup, db_backup):
|
def _from_db_object(cls, context, backup, db_backup, expected_attrs=None):
|
||||||
|
if expected_attrs is None:
|
||||||
|
expected_attrs = []
|
||||||
for name, field in backup.fields.items():
|
for name, field in backup.fields.items():
|
||||||
|
if name in cls.OPTIONAL_FIELDS:
|
||||||
|
continue
|
||||||
value = db_backup.get(name)
|
value = db_backup.get(name)
|
||||||
if isinstance(field, fields.IntegerField):
|
if isinstance(field, fields.IntegerField):
|
||||||
value = value if value is not None else 0
|
value = value if value is not None else 0
|
||||||
backup[name] = value
|
backup[name] = value
|
||||||
|
|
||||||
|
if 'metadata' in expected_attrs:
|
||||||
|
metadata = db_backup.get('backup_metadata')
|
||||||
|
if metadata is None:
|
||||||
|
raise exception.MetadataAbsent()
|
||||||
|
backup.metadata = {item['key']: item['value']
|
||||||
|
for item in metadata}
|
||||||
|
|
||||||
backup._context = context
|
backup._context = context
|
||||||
backup.obj_reset_changes()
|
backup.obj_reset_changes()
|
||||||
return backup
|
return backup
|
||||||
|
|
||||||
|
def obj_reset_changes(self, fields=None):
|
||||||
|
super(Backup, self).obj_reset_changes(fields)
|
||||||
|
self._reset_metadata_tracking(fields=fields)
|
||||||
|
|
||||||
|
def obj_load_attr(self, attrname):
|
||||||
|
if attrname not in self.OPTIONAL_FIELDS:
|
||||||
|
raise exception.ObjectActionError(
|
||||||
|
action='obj_load_attr',
|
||||||
|
reason=_('attribute %s not lazy-loadable') % attrname)
|
||||||
|
if not self._context:
|
||||||
|
raise exception.OrphanedObjectError(method='obj_load_attr',
|
||||||
|
objtype=self.obj_name())
|
||||||
|
self.obj_reset_changes(fields=[attrname])
|
||||||
|
|
||||||
|
def obj_what_changed(self):
|
||||||
|
changes = super(Backup, self).obj_what_changed()
|
||||||
|
if hasattr(self, 'metadata') and self.metadata != self._orig_metadata:
|
||||||
|
changes.add('metadata')
|
||||||
|
|
||||||
|
return changes
|
||||||
|
|
||||||
def create(self):
|
def create(self):
|
||||||
if self.obj_attr_is_set('id'):
|
if self.obj_attr_is_set('id'):
|
||||||
raise exception.ObjectActionError(action='create',
|
raise exception.ObjectActionError(action='create',
|
||||||
@ -116,6 +167,11 @@ class Backup(base.CinderPersistentObject, base.CinderObject,
|
|||||||
def save(self):
|
def save(self):
|
||||||
updates = self.cinder_obj_get_changes()
|
updates = self.cinder_obj_get_changes()
|
||||||
if updates:
|
if updates:
|
||||||
|
if 'metadata' in updates:
|
||||||
|
metadata = updates.pop('metadata', None)
|
||||||
|
self.metadata = db.backup_metadata_update(self._context,
|
||||||
|
self.id, metadata,
|
||||||
|
True)
|
||||||
db.backup_update(self._context, self.id, updates)
|
db.backup_update(self._context, self.id, updates)
|
||||||
|
|
||||||
self.obj_reset_changes()
|
self.obj_reset_changes()
|
||||||
@ -166,14 +222,16 @@ class BackupList(base.ObjectListBase, base.CinderObject):
|
|||||||
offset=None, sort_keys=None, sort_dirs=None):
|
offset=None, sort_keys=None, sort_dirs=None):
|
||||||
backups = db.backup_get_all(context, filters, marker, limit, offset,
|
backups = db.backup_get_all(context, filters, marker, limit, offset,
|
||||||
sort_keys, sort_dirs)
|
sort_keys, sort_dirs)
|
||||||
|
expected_attrs = Backup._get_expected_attrs(context)
|
||||||
return base.obj_make_list(context, cls(context), objects.Backup,
|
return base.obj_make_list(context, cls(context), objects.Backup,
|
||||||
backups)
|
backups, expected_attrs=expected_attrs)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_all_by_host(cls, context, host):
|
def get_all_by_host(cls, context, host):
|
||||||
backups = db.backup_get_all_by_host(context, host)
|
backups = db.backup_get_all_by_host(context, host)
|
||||||
|
expected_attrs = Backup._get_expected_attrs(context)
|
||||||
return base.obj_make_list(context, cls(context), objects.Backup,
|
return base.obj_make_list(context, cls(context), objects.Backup,
|
||||||
backups)
|
backups, expected_attrs=expected_attrs)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_all_by_project(cls, context, project_id, filters=None,
|
def get_all_by_project(cls, context, project_id, filters=None,
|
||||||
@ -182,20 +240,23 @@ class BackupList(base.ObjectListBase, base.CinderObject):
|
|||||||
backups = db.backup_get_all_by_project(context, project_id, filters,
|
backups = db.backup_get_all_by_project(context, project_id, filters,
|
||||||
marker, limit, offset,
|
marker, limit, offset,
|
||||||
sort_keys, sort_dirs)
|
sort_keys, sort_dirs)
|
||||||
|
expected_attrs = Backup._get_expected_attrs(context)
|
||||||
return base.obj_make_list(context, cls(context), objects.Backup,
|
return base.obj_make_list(context, cls(context), objects.Backup,
|
||||||
backups)
|
backups, expected_attrs=expected_attrs)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_all_by_volume(cls, context, volume_id, filters=None):
|
def get_all_by_volume(cls, context, volume_id, filters=None):
|
||||||
backups = db.backup_get_all_by_volume(context, volume_id, filters)
|
backups = db.backup_get_all_by_volume(context, volume_id, filters)
|
||||||
|
expected_attrs = Backup._get_expected_attrs(context)
|
||||||
return base.obj_make_list(context, cls(context), objects.Backup,
|
return base.obj_make_list(context, cls(context), objects.Backup,
|
||||||
backups)
|
backups, expected_attrs=expected_attrs)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_all_active_by_window(cls, context, begin, end):
|
def get_all_active_by_window(cls, context, begin, end):
|
||||||
backups = db.backup_get_all_active_by_window(context, begin, end)
|
backups = db.backup_get_all_active_by_window(context, begin, end)
|
||||||
|
expected_attrs = Backup._get_expected_attrs(context)
|
||||||
return base.obj_make_list(context, cls(context), objects.Backup,
|
return base.obj_make_list(context, cls(context), objects.Backup,
|
||||||
backups)
|
backups, expected_attrs=expected_attrs)
|
||||||
|
|
||||||
|
|
||||||
@base.CinderObjectRegistry.register
|
@base.CinderObjectRegistry.register
|
||||||
|
@ -134,6 +134,7 @@ OBJ_VERSIONS.add('1.23', {'VolumeAttachment': '1.2'})
|
|||||||
OBJ_VERSIONS.add('1.24', {'LogLevel': '1.0', 'LogLevelList': '1.0'})
|
OBJ_VERSIONS.add('1.24', {'LogLevel': '1.0', 'LogLevelList': '1.0'})
|
||||||
OBJ_VERSIONS.add('1.25', {'Group': '1.2'})
|
OBJ_VERSIONS.add('1.25', {'Group': '1.2'})
|
||||||
OBJ_VERSIONS.add('1.26', {'Snapshot': '1.5'})
|
OBJ_VERSIONS.add('1.26', {'Snapshot': '1.5'})
|
||||||
|
OBJ_VERSIONS.add('1.27', {'Backup': '1.5', 'BackupImport': '1.5'})
|
||||||
|
|
||||||
|
|
||||||
class CinderObjectRegistry(base.VersionedObjectRegistry):
|
class CinderObjectRegistry(base.VersionedObjectRegistry):
|
||||||
|
@ -260,10 +260,7 @@ class AdminActionsTest(BaseAdminTest):
|
|||||||
|
|
||||||
def test_backup_reset_status_as_non_admin(self):
|
def test_backup_reset_status_as_non_admin(self):
|
||||||
ctx = context.RequestContext(fake.USER_ID, fake.PROJECT_ID)
|
ctx = context.RequestContext(fake.USER_ID, fake.PROJECT_ID)
|
||||||
backup = db.backup_create(ctx, {'status': 'available',
|
backup = test_utils.create_backup(ctx, status='available')
|
||||||
'size': 1,
|
|
||||||
'volume_id': "fakeid",
|
|
||||||
'host': 'test'})
|
|
||||||
resp = self._issue_backup_reset(ctx,
|
resp = self._issue_backup_reset(ctx,
|
||||||
backup,
|
backup,
|
||||||
{'status': fields.BackupStatus.ERROR})
|
{'status': fields.BackupStatus.ERROR})
|
||||||
|
@ -25,6 +25,7 @@ from six.moves import http_client
|
|||||||
import webob
|
import webob
|
||||||
|
|
||||||
from cinder.api.contrib import backups
|
from cinder.api.contrib import backups
|
||||||
|
from cinder.api.openstack import api_version_request as api_version
|
||||||
# needed for stubs to work
|
# needed for stubs to work
|
||||||
import cinder.backup
|
import cinder.backup
|
||||||
from cinder.backup import api as backup_api
|
from cinder.backup import api as backup_api
|
||||||
@ -109,6 +110,23 @@ class BackupsAPITestCase(test.TestCase):
|
|||||||
backup.destroy()
|
backup.destroy()
|
||||||
volume.destroy()
|
volume.destroy()
|
||||||
|
|
||||||
|
def test_show_backup_return_metadata(self):
|
||||||
|
volume = utils.create_volume(self.context, size=5, status='creating')
|
||||||
|
backup = utils.create_backup(self.context, volume.id,
|
||||||
|
metadata={"test_key": "test_value"})
|
||||||
|
req = webob.Request.blank('/v3/%s/backups/%s' % (
|
||||||
|
fake.PROJECT_ID, backup.id))
|
||||||
|
req.method = 'GET'
|
||||||
|
req.headers['Content-Type'] = 'application/json'
|
||||||
|
req.headers['OpenStack-API-Version'] = 'volume 3.43'
|
||||||
|
res = req.get_response(fakes.wsgi_app(
|
||||||
|
fake_auth_context=self.user_context))
|
||||||
|
res_dict = jsonutils.loads(res.body)
|
||||||
|
self.assertEqual({"test_key": "test_value"},
|
||||||
|
res_dict['backup']['metadata'])
|
||||||
|
volume.destroy()
|
||||||
|
backup.destroy()
|
||||||
|
|
||||||
def test_show_backup_with_backup_NotFound(self):
|
def test_show_backup_with_backup_NotFound(self):
|
||||||
req = webob.Request.blank('/v2/%s/backups/%s' % (
|
req = webob.Request.blank('/v2/%s/backups/%s' % (
|
||||||
fake.PROJECT_ID, fake.WILL_NOT_BE_FOUND_ID))
|
fake.PROJECT_ID, fake.WILL_NOT_BE_FOUND_ID))
|
||||||
@ -303,6 +321,33 @@ class BackupsAPITestCase(test.TestCase):
|
|||||||
backup2.destroy()
|
backup2.destroy()
|
||||||
backup1.destroy()
|
backup1.destroy()
|
||||||
|
|
||||||
|
def test_list_backups_detail_return_metadata(self):
|
||||||
|
backup1 = utils.create_backup(self.context, size=1,
|
||||||
|
metadata={'key1': 'value1'})
|
||||||
|
backup2 = utils.create_backup(self.context, size=1,
|
||||||
|
metadata={'key2': 'value2'})
|
||||||
|
backup3 = utils.create_backup(self.context, size=1)
|
||||||
|
|
||||||
|
req = webob.Request.blank('/v3/%s/backups/detail' % fake.PROJECT_ID)
|
||||||
|
req.method = 'GET'
|
||||||
|
req.headers['Content-Type'] = 'application/json'
|
||||||
|
req.headers['Accept'] = 'application/json'
|
||||||
|
req.headers['OpenStack-API-Version'] = 'volume 3.43'
|
||||||
|
res = req.get_response(fakes.wsgi_app(
|
||||||
|
fake_auth_context=self.user_context))
|
||||||
|
res_dict = jsonutils.loads(res.body)
|
||||||
|
|
||||||
|
self.assertEqual({'key1': 'value1'},
|
||||||
|
res_dict['backups'][2]['metadata'])
|
||||||
|
self.assertEqual({'key2': 'value2'},
|
||||||
|
res_dict['backups'][1]['metadata'])
|
||||||
|
self.assertEqual({},
|
||||||
|
res_dict['backups'][0]['metadata'])
|
||||||
|
|
||||||
|
backup3.destroy()
|
||||||
|
backup2.destroy()
|
||||||
|
backup1.destroy()
|
||||||
|
|
||||||
def test_list_backups_detail_using_filters(self):
|
def test_list_backups_detail_using_filters(self):
|
||||||
backup1 = utils.create_backup(self.context, display_name='test2')
|
backup1 = utils.create_backup(self.context, display_name='test2')
|
||||||
backup2 = utils.create_backup(self.context,
|
backup2 = utils.create_backup(self.context,
|
||||||
@ -470,6 +515,48 @@ class BackupsAPITestCase(test.TestCase):
|
|||||||
|
|
||||||
volume.destroy()
|
volume.destroy()
|
||||||
|
|
||||||
|
@mock.patch('cinder.db.service_get_all')
|
||||||
|
@mock.patch(
|
||||||
|
'cinder.api.openstack.wsgi.Controller.validate_name_and_description')
|
||||||
|
def test_create_backup_with_metadata(self, mock_validate,
|
||||||
|
_mock_service_get_all):
|
||||||
|
_mock_service_get_all.return_value = [
|
||||||
|
{'availability_zone': 'fake_az', 'host': 'testhost',
|
||||||
|
'disabled': 0, 'updated_at': timeutils.utcnow()}]
|
||||||
|
|
||||||
|
volume = utils.create_volume(self.context, size=1)
|
||||||
|
# Create a backup with metadata
|
||||||
|
body = {"backup": {"display_name": "nightly001",
|
||||||
|
"display_description":
|
||||||
|
"Nightly Backup 03-Sep-2012",
|
||||||
|
"volume_id": volume.id,
|
||||||
|
"container": "nightlybackups",
|
||||||
|
'metadata': {'test_key': 'test_value'}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
req = webob.Request.blank('/v3/%s/backups' % fake.PROJECT_ID)
|
||||||
|
req.method = 'POST'
|
||||||
|
req.headers['Content-Type'] = 'application/json'
|
||||||
|
req.headers['OpenStack-API-Version'] = 'volume 3.43'
|
||||||
|
req.body = jsonutils.dump_as_bytes(body)
|
||||||
|
res = req.get_response(fakes.wsgi_app(
|
||||||
|
fake_auth_context=self.user_context))
|
||||||
|
res_dict = jsonutils.loads(res.body)
|
||||||
|
# Get the new backup
|
||||||
|
req = webob.Request.blank('/v3/%s/backups/%s' % (
|
||||||
|
fake.PROJECT_ID, res_dict['backup']['id']))
|
||||||
|
req.method = 'GET'
|
||||||
|
req.headers['Content-Type'] = 'application/json'
|
||||||
|
req.headers['OpenStack-API-Version'] = 'volume 3.43'
|
||||||
|
res = req.get_response(fakes.wsgi_app(
|
||||||
|
fake_auth_context=self.user_context))
|
||||||
|
res_dict = jsonutils.loads(res.body)
|
||||||
|
|
||||||
|
self.assertEqual({'test_key': 'test_value'},
|
||||||
|
res_dict['backup']['metadata'])
|
||||||
|
|
||||||
|
volume.destroy()
|
||||||
|
|
||||||
@mock.patch('cinder.db.service_get_all')
|
@mock.patch('cinder.db.service_get_all')
|
||||||
def test_create_backup_inuse_no_force(self,
|
def test_create_backup_inuse_no_force(self,
|
||||||
_mock_service_get_all):
|
_mock_service_get_all):
|
||||||
@ -666,6 +753,7 @@ class BackupsAPITestCase(test.TestCase):
|
|||||||
req = webob.Request.blank('/v2/%s/backups' % fake.PROJECT_ID)
|
req = webob.Request.blank('/v2/%s/backups' % fake.PROJECT_ID)
|
||||||
req.method = 'POST'
|
req.method = 'POST'
|
||||||
req.environ['cinder.context'] = self.context
|
req.environ['cinder.context'] = self.context
|
||||||
|
req.api_version_request = api_version.APIVersionRequest()
|
||||||
self.assertRaises(exception.InvalidInput,
|
self.assertRaises(exception.InvalidInput,
|
||||||
self.controller.create,
|
self.controller.create,
|
||||||
req,
|
req,
|
||||||
|
@ -81,7 +81,8 @@ class BaseBackupTest(test.TestCase):
|
|||||||
service=None,
|
service=None,
|
||||||
temp_volume_id=None,
|
temp_volume_id=None,
|
||||||
temp_snapshot_id=None,
|
temp_snapshot_id=None,
|
||||||
snapshot_id=None):
|
snapshot_id=None,
|
||||||
|
metadata=None):
|
||||||
"""Create a backup entry in the DB.
|
"""Create a backup entry in the DB.
|
||||||
|
|
||||||
Return the entry ID
|
Return the entry ID
|
||||||
@ -105,6 +106,7 @@ class BaseBackupTest(test.TestCase):
|
|||||||
kwargs['object_count'] = object_count
|
kwargs['object_count'] = object_count
|
||||||
kwargs['temp_volume_id'] = temp_volume_id
|
kwargs['temp_volume_id'] = temp_volume_id
|
||||||
kwargs['temp_snapshot_id'] = temp_snapshot_id
|
kwargs['temp_snapshot_id'] = temp_snapshot_id
|
||||||
|
kwargs['metadata'] = metadata or {}
|
||||||
backup = objects.Backup(context=self.ctxt, **kwargs)
|
backup = objects.Backup(context=self.ctxt, **kwargs)
|
||||||
backup.create()
|
backup.create()
|
||||||
return backup
|
return backup
|
||||||
|
@ -42,6 +42,7 @@ fake_backup = {
|
|||||||
'snapshot_id': None,
|
'snapshot_id': None,
|
||||||
'data_timestamp': None,
|
'data_timestamp': None,
|
||||||
'restore_volume_id': None,
|
'restore_volume_id': None,
|
||||||
|
'backup_metadata': {},
|
||||||
}
|
}
|
||||||
|
|
||||||
vol_props = {'status': 'available', 'size': 1}
|
vol_props = {'status': 'available', 'size': 1}
|
||||||
@ -65,8 +66,10 @@ class TestBackup(test_objects.BaseObjectsTestCase):
|
|||||||
def test_get_by_id_no_existing_id(self, model_query):
|
def test_get_by_id_no_existing_id(self, model_query):
|
||||||
query = mock.Mock()
|
query = mock.Mock()
|
||||||
filter_by = mock.Mock()
|
filter_by = mock.Mock()
|
||||||
|
query_options = mock.Mock()
|
||||||
filter_by.first.return_value = None
|
filter_by.first.return_value = None
|
||||||
query.filter_by.return_value = filter_by
|
query_options.filter_by.return_value = filter_by
|
||||||
|
query.options.return_value = query_options
|
||||||
model_query.return_value = query
|
model_query.return_value = query
|
||||||
self.assertRaises(exception.BackupNotFound, objects.Backup.get_by_id,
|
self.assertRaises(exception.BackupNotFound, objects.Backup.get_by_id,
|
||||||
self.context, 123)
|
self.context, 123)
|
||||||
@ -87,6 +90,20 @@ class TestBackup(test_objects.BaseObjectsTestCase):
|
|||||||
backup_update.assert_called_once_with(self.context, backup.id,
|
backup_update.assert_called_once_with(self.context, backup.id,
|
||||||
{'display_name': 'foobar'})
|
{'display_name': 'foobar'})
|
||||||
|
|
||||||
|
@mock.patch('cinder.db.backup_metadata_update',
|
||||||
|
return_value={'key1': 'value1'})
|
||||||
|
@mock.patch('cinder.db.backup_update')
|
||||||
|
def test_save_with_metadata(self, backup_update, metadata_update):
|
||||||
|
backup = objects.Backup._from_db_object(
|
||||||
|
self.context, objects.Backup(), fake_backup)
|
||||||
|
|
||||||
|
backup.metadata = {'key1': 'value1'}
|
||||||
|
self.assertEqual({'metadata': {'key1': 'value1'}},
|
||||||
|
backup.obj_get_changes())
|
||||||
|
backup.save()
|
||||||
|
metadata_update.assert_called_once_with(self.context, backup.id,
|
||||||
|
{'key1': 'value1'}, True)
|
||||||
|
|
||||||
@mock.patch('oslo_utils.timeutils.utcnow', return_value=timeutils.utcnow())
|
@mock.patch('oslo_utils.timeutils.utcnow', return_value=timeutils.utcnow())
|
||||||
@mock.patch('cinder.db.sqlalchemy.api.backup_destroy')
|
@mock.patch('cinder.db.sqlalchemy.api.backup_destroy')
|
||||||
def test_destroy(self, backup_destroy, utcnow_mock):
|
def test_destroy(self, backup_destroy, utcnow_mock):
|
||||||
@ -121,6 +138,11 @@ class TestBackup(test_objects.BaseObjectsTestCase):
|
|||||||
restore_volume_id='2')
|
restore_volume_id='2')
|
||||||
self.assertEqual('2', backup.restore_volume_id)
|
self.assertEqual('2', backup.restore_volume_id)
|
||||||
|
|
||||||
|
def test_obj_field_metadata(self):
|
||||||
|
backup = objects.Backup(context=self.context,
|
||||||
|
metadata={'test_key': 'test_value'})
|
||||||
|
self.assertEqual({'test_key': 'test_value'}, backup.metadata)
|
||||||
|
|
||||||
def test_import_record(self):
|
def test_import_record(self):
|
||||||
utils.replace_obj_loader(self, objects.Backup)
|
utils.replace_obj_loader(self, objects.Backup)
|
||||||
backup = objects.Backup(context=self.context, id=fake.BACKUP_ID,
|
backup = objects.Backup(context=self.context, id=fake.BACKUP_ID,
|
||||||
@ -222,10 +244,7 @@ class TestBackupList(test_objects.BaseObjectsTestCase):
|
|||||||
@mock.patch('cinder.db.backup_get_all_by_host',
|
@mock.patch('cinder.db.backup_get_all_by_host',
|
||||||
return_value=[fake_backup])
|
return_value=[fake_backup])
|
||||||
def test_get_all_by_host(self, get_all_by_host):
|
def test_get_all_by_host(self, get_all_by_host):
|
||||||
fake_volume_obj = fake_volume.fake_volume_obj(self.context)
|
backups = objects.BackupList.get_all_by_host(self.context, "fake_host")
|
||||||
|
|
||||||
backups = objects.BackupList.get_all_by_host(self.context,
|
|
||||||
fake_volume_obj.id)
|
|
||||||
self.assertEqual(1, len(backups))
|
self.assertEqual(1, len(backups))
|
||||||
TestBackup._compare(self, fake_backup, backups[0])
|
TestBackup._compare(self, fake_backup, backups[0])
|
||||||
|
|
||||||
|
@ -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.4-c50f7a68bb4c400dd53dd219685b3992',
|
'Backup': '1.5-3ab4b305bd43ec0cff6701fe2a849194',
|
||||||
'BackupDeviceInfo': '1.0-74b3950676c690538f4bc6796bd0042e',
|
'BackupDeviceInfo': '1.0-74b3950676c690538f4bc6796bd0042e',
|
||||||
'BackupImport': '1.4-c50f7a68bb4c400dd53dd219685b3992',
|
'BackupImport': '1.5-3ab4b305bd43ec0cff6701fe2a849194',
|
||||||
'BackupList': '1.0-15ecf022a68ddbb8c2a6739cfc9f8f5e',
|
'BackupList': '1.0-15ecf022a68ddbb8c2a6739cfc9f8f5e',
|
||||||
'CleanupRequest': '1.0-e7c688b893e1d5537ccf65cc3eb10a28',
|
'CleanupRequest': '1.0-e7c688b893e1d5537ccf65cc3eb10a28',
|
||||||
'Cluster': '1.1-e2c533eb8cdd8d229b6c45c6cf3a9e2c',
|
'Cluster': '1.1-e2c533eb8cdd8d229b6c45c6cf3a9e2c',
|
||||||
@ -126,7 +126,9 @@ class TestObjectVersions(test.TestCase):
|
|||||||
# the converted data, but at least ensures the method doesn't blow
|
# the converted data, but at least ensures the method doesn't blow
|
||||||
# up on something simple.
|
# up on something simple.
|
||||||
init_args = {}
|
init_args = {}
|
||||||
init_kwargs = {objects.Snapshot: {'context': 'ctxt'}}
|
init_kwargs = {objects.Snapshot: {'context': 'ctxt'},
|
||||||
|
objects.Backup: {'context': 'ctxt'},
|
||||||
|
objects.BackupImport: {'context': 'ctxt'}}
|
||||||
checker = fixture.ObjectVersionChecker(
|
checker = fixture.ObjectVersionChecker(
|
||||||
base.CinderObjectRegistry.obj_classes())
|
base.CinderObjectRegistry.obj_classes())
|
||||||
checker.test_compatibility_routines(init_args=init_args,
|
checker.test_compatibility_routines(init_args=init_args,
|
||||||
|
@ -552,6 +552,7 @@ class TestCinderManageCmd(test.TestCase):
|
|||||||
'size': 123,
|
'size': 123,
|
||||||
'object_count': 1,
|
'object_count': 1,
|
||||||
'volume_id': fake.VOLUME_ID,
|
'volume_id': fake.VOLUME_ID,
|
||||||
|
'backup_metadata': {},
|
||||||
}
|
}
|
||||||
backup_get_all.return_value = [backup]
|
backup_get_all.return_value = [backup]
|
||||||
with mock.patch('sys.stdout', new=six.StringIO()) as fake_out:
|
with mock.patch('sys.stdout', new=six.StringIO()) as fake_out:
|
||||||
@ -605,6 +606,7 @@ class TestCinderManageCmd(test.TestCase):
|
|||||||
'size': 123,
|
'size': 123,
|
||||||
'object_count': 1,
|
'object_count': 1,
|
||||||
'volume_id': fake.VOLUME_ID,
|
'volume_id': fake.VOLUME_ID,
|
||||||
|
'backup_metadata': {},
|
||||||
}
|
}
|
||||||
backup_get_by_host.return_value = [backup]
|
backup_get_by_host.return_value = [backup]
|
||||||
backup_cmds = cinder_manage.BackupCommands()
|
backup_cmds = cinder_manage.BackupCommands()
|
||||||
|
@ -2516,7 +2516,7 @@ class DBAPIBackupTestCase(BaseTest):
|
|||||||
"""Tests for db.api.backup_* methods."""
|
"""Tests for db.api.backup_* methods."""
|
||||||
|
|
||||||
_ignored_keys = ['id', 'deleted', 'deleted_at', 'created_at',
|
_ignored_keys = ['id', 'deleted', 'deleted_at', 'created_at',
|
||||||
'updated_at', 'data_timestamp']
|
'updated_at', 'data_timestamp', 'backup_metadata']
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(DBAPIBackupTestCase, self).setUp()
|
super(DBAPIBackupTestCase, self).setUp()
|
||||||
|
@ -1261,6 +1261,30 @@ class MigrationsMixin(test_migrations.WalkVersionsMixin):
|
|||||||
messages = db_utils.get_table(engine, 'messages')
|
messages = db_utils.get_table(engine, 'messages')
|
||||||
self.assertEqual(255, messages.c.project_id.type.length)
|
self.assertEqual(255, messages.c.project_id.type.length)
|
||||||
|
|
||||||
|
def _check_105(self, engine, data):
|
||||||
|
self.assertTrue(engine.dialect.has_table(engine.connect(),
|
||||||
|
"backup_metadata"))
|
||||||
|
backup_metadata = db_utils.get_table(engine, 'backup_metadata')
|
||||||
|
|
||||||
|
self.assertIsInstance(backup_metadata.c.created_at.type,
|
||||||
|
self.TIME_TYPE)
|
||||||
|
self.assertIsInstance(backup_metadata.c.updated_at.type,
|
||||||
|
self.TIME_TYPE)
|
||||||
|
self.assertIsInstance(backup_metadata.c.deleted_at.type,
|
||||||
|
self.TIME_TYPE)
|
||||||
|
self.assertIsInstance(backup_metadata.c.deleted.type,
|
||||||
|
self.BOOL_TYPE)
|
||||||
|
self.assertIsInstance(backup_metadata.c.id.type,
|
||||||
|
self.INTEGER_TYPE)
|
||||||
|
self.assertIsInstance(backup_metadata.c.key.type,
|
||||||
|
self.VARCHAR_TYPE)
|
||||||
|
self.assertIsInstance(backup_metadata.c.value.type,
|
||||||
|
self.VARCHAR_TYPE)
|
||||||
|
self.assertIsInstance(backup_metadata.c.backup_id.type,
|
||||||
|
self.VARCHAR_TYPE)
|
||||||
|
f_keys = self.get_foreign_key_columns(engine, 'backup_metadata')
|
||||||
|
self.assertEqual({'backup_id'}, f_keys)
|
||||||
|
|
||||||
def test_walk_versions(self):
|
def test_walk_versions(self):
|
||||||
self.walk_versions(False, False)
|
self.walk_versions(False, False)
|
||||||
self.assert_each_foreign_key_is_part_of_an_index()
|
self.assert_each_foreign_key_is_part_of_an_index()
|
||||||
|
@ -344,6 +344,7 @@ def create_backup(ctxt,
|
|||||||
container=None,
|
container=None,
|
||||||
availability_zone=None,
|
availability_zone=None,
|
||||||
host=None,
|
host=None,
|
||||||
|
metadata=None,
|
||||||
**kwargs):
|
**kwargs):
|
||||||
"""Create a backup object."""
|
"""Create a backup object."""
|
||||||
values = {
|
values = {
|
||||||
@ -363,7 +364,8 @@ def create_backup(ctxt,
|
|||||||
'temp_volume_id': temp_volume_id,
|
'temp_volume_id': temp_volume_id,
|
||||||
'temp_snapshot_id': temp_snapshot_id,
|
'temp_snapshot_id': temp_snapshot_id,
|
||||||
'snapshot_id': snapshot_id,
|
'snapshot_id': snapshot_id,
|
||||||
'data_timestamp': data_timestamp, }
|
'data_timestamp': data_timestamp,
|
||||||
|
'metadata': metadata or {}, }
|
||||||
|
|
||||||
values.update(kwargs)
|
values.update(kwargs)
|
||||||
backup = objects.Backup(ctxt, **values)
|
backup = objects.Backup(ctxt, **values)
|
||||||
|
Loading…
Reference in New Issue
Block a user