Metadata for Share Snapshots Resource

This change adds metadata controller for Snapshots resource
Bumps microversion to 2.73

APIImpact
Partially-implements: bp/metadata-for-share-resources

Change-Id: I91151792d033a4297557cd5f330053d78895eb78
This commit is contained in:
Ashley Rodriguez 2022-03-04 18:02:56 +00:00
parent 6b7bc9f37b
commit 206885a3e9
19 changed files with 691 additions and 21 deletions

View File

@ -187,6 +187,7 @@ REST_API_VERSION_HISTORY = """
network.
* 2.71 - Added 'updated_at' field in share instance show API output.
* 2.72 - Added new option ``share-network`` to share replica creare API.
* 2.73 - Added Share Snapshot Metadata to Metadata API
"""
@ -194,7 +195,7 @@ REST_API_VERSION_HISTORY = """
# The default api version request is defined to be the
# minimum version of the API supported.
_MIN_API_VERSION = "2.0"
_MAX_API_VERSION = "2.72"
_MAX_API_VERSION = "2.73"
DEFAULT_API_VERSION = _MIN_API_VERSION

View File

@ -399,4 +399,10 @@ ____
2.72
----
Added 'share_network' option to share replica create API.
2.73
----
Added Metadata API methods (GET, PUT, POST, DELETE)
to Share Snapshots

View File

@ -15,6 +15,7 @@
"""The share snapshots api."""
import ast
from http import client as http_client
from oslo_log import log
@ -22,6 +23,7 @@ import webob
from webob import exc
from manila.api import common
from manila.api.openstack import api_version_request as api_version
from manila.api.openstack import wsgi
from manila.api.views import share_snapshots as snapshot_views
from manila import db
@ -114,6 +116,18 @@ class ShareSnapshotMixin(object):
search_opts['display_description'] = search_opts.pop(
'description')
# Deserialize dicts
if req.api_version_request >= api_version.APIVersionRequest("2.73"):
if 'metadata' in search_opts:
try:
search_opts['metadata'] = ast.literal_eval(
search_opts['metadata'])
except ValueError:
msg = _('Invalid value for metadata filter.')
raise webob.exc.HTTPBadRequest(explanation=msg)
else:
search_opts.pop('metadata', None)
# like filter
for key, db_key in (('name~', 'display_name~'),
('description~', 'display_description~')):
@ -141,7 +155,7 @@ class ShareSnapshotMixin(object):
def _get_snapshots_search_options(self):
"""Return share snapshot search options allowed by non-admin."""
return ('display_name', 'status', 'share_id', 'size', 'display_name~',
'display_description~', 'display_description')
'display_description~', 'display_description', 'metadata')
def update(self, req, id, body):
"""Update a snapshot."""
@ -212,11 +226,20 @@ class ShareSnapshotMixin(object):
snapshot['display_description'] = snapshot.get('description')
del snapshot['description']
kwargs = {}
if req.api_version_request >= api_version.APIVersionRequest("2.73"):
if snapshot.get('metadata'):
metadata = snapshot.get('metadata')
kwargs.update({
'metadata': metadata,
})
new_snapshot = self.share_api.create_snapshot(
context,
share,
snapshot.get('display_name'),
snapshot.get('display_description'))
snapshot.get('display_description'),
**kwargs)
return self._view_builder.detail(
req, dict(new_snapshot.items()))

View File

@ -26,30 +26,37 @@ class MetadataController(object):
# From db, ensure it exists
resource_get = {
"share": "share_get",
"share_snapshot": "share_snapshot_get",
}
resource_metadata_get = {
"share": "share_metadata_get",
"share_snapshot": "share_snapshot_metadata_get",
}
resource_metadata_get_item = {
"share": "share_metadata_get_item",
"share_snapshot": "share_snapshot_metadata_get_item",
}
resource_metadata_update = {
"share": "share_metadata_update",
"share_snapshot": "share_snapshot_metadata_update",
}
resource_metadata_update_item = {
"share": "share_metadata_update_item",
"share_snapshot": "share_snapshot_metadata_update_item",
}
resource_metadata_delete = {
"share": "share_metadata_delete",
"share_snapshot": "share_snapshot_metadata_delete",
}
resource_policy_get = {
'share': 'get',
'share_snapshot': 'get_snapshot',
}
def __init__(self):
@ -60,7 +67,8 @@ class MetadataController(object):
for_modification=False, parent_id=None):
if self.resource_name in ['share']:
# we would allow retrieving some "public" resources
# across project namespaces
# across project namespaces excpet share snaphots,
# project_only=True is hard coded
kwargs = {}
else:
kwargs = {'project_only': True}

View File

@ -255,6 +255,44 @@ class APIRouter(manila.api.openstack.APIRouter):
controller=self.resources["snapshots"],
collection={"detail": "GET"},
member={"action": "POST"})
for path_prefix in ['/{project_id}', '']:
# project_id is optional
mapper.connect("snapshots_metadata",
"%s/snapshots/{resource_id}/metadata"
% path_prefix,
controller=self.resources["snapshots"],
action="create_metadata",
conditions={"method": ["POST"]})
mapper.connect("snapshots_metadata",
"%s/snapshots/{resource_id}/metadata"
% path_prefix,
controller=self.resources["snapshots"],
action="update_all_metadata",
conditions={"method": ["PUT"]})
mapper.connect("snapshots_metadata",
"%s/snapshots/{resource_id}/metadata/{key}"
% path_prefix,
controller=self.resources["snapshots"],
action="update_metadata_item",
conditions={"method": ["POST"]})
mapper.connect("snapshots_metadata",
"%s/snapshots/{resource_id}/metadata"
% path_prefix,
controller=self.resources["snapshots"],
action="index_metadata",
conditions={"method": ["GET"]})
mapper.connect("snapshots_metadata",
"%s/snapshots/{resource_id}/metadata/{key}"
% path_prefix,
controller=self.resources["snapshots"],
action="show_metadata",
conditions={"method": ["GET"]})
mapper.connect("snapshots_metadata",
"%s/snapshots/{resource_id}/metadata/{key}"
% path_prefix,
controller=self.resources["snapshots"],
action="delete_metadata",
conditions={"method": ["DELETE"]})
for path_prefix in ['/{project_id}', '']:
# project_id is optional

View File

@ -26,6 +26,7 @@ from manila.api import common
from manila.api.openstack import api_version_request as api_version
from manila.api.openstack import wsgi
from manila.api.v1 import share_snapshots
from manila.api.v2 import metadata
from manila.api.views import share_snapshots as snapshot_views
from manila.common import constants
from manila.db import api as db_api
@ -37,7 +38,8 @@ LOG = log.getLogger(__name__)
class ShareSnapshotsController(share_snapshots.ShareSnapshotMixin,
wsgi.Controller, wsgi.AdminActionsMixin):
wsgi.Controller, metadata.MetadataController,
wsgi.AdminActionsMixin):
"""The Share Snapshots API V2 controller for the OpenStack API."""
resource_name = 'share_snapshot'
@ -123,6 +125,12 @@ class ShareSnapshotsController(share_snapshots.ShareSnapshotMixin,
'display_name': name,
'display_description': description,
}
if req.api_version_request >= api_version.APIVersionRequest("2.73"):
if snapshot_data.get('metadata'):
metadata = snapshot_data.get('metadata')
snapshot.update({
'metadata': metadata,
})
try:
share_ref = self.share_api.get(context, share_id)
@ -339,6 +347,37 @@ class ShareSnapshotsController(share_snapshots.ShareSnapshotMixin,
req.GET.pop('description', None)
return self._get_snapshots(req, is_detail=True)
@wsgi.Controller.api_version("2.73")
@wsgi.Controller.authorize("get_metadata")
def index_metadata(self, req, resource_id):
"""Returns the list of metadata for a given share snapshot."""
return self._index_metadata(req, resource_id)
@wsgi.Controller.api_version("2.73")
@wsgi.Controller.authorize("update_metadata")
def create_metadata(self, req, resource_id, body):
return self._create_metadata(req, resource_id, body)
@wsgi.Controller.api_version("2.73")
@wsgi.Controller.authorize("update_metadata")
def update_all_metadata(self, req, resource_id, body):
return self._update_all_metadata(req, resource_id, body)
@wsgi.Controller.api_version("2.73")
@wsgi.Controller.authorize("update_metadata")
def update_metadata_item(self, req, resource_id, body, key):
return self._update_metadata_item(req, resource_id, body, key)
@wsgi.Controller.api_version("2.73")
@wsgi.Controller.authorize("get_metadata")
def show_metadata(self, req, resource_id, key):
return self._show_metadata(req, resource_id, key)
@wsgi.Controller.api_version("2.73")
@wsgi.Controller.authorize("delete_metadata")
def delete_metadata(self, req, resource_id, key):
return self._delete_metadata(req, resource_id, key)
def create_resource():
return wsgi.Resource(ShareSnapshotsController())

View File

@ -23,6 +23,7 @@ class ViewBuilder(common.ViewBuilder):
_detail_version_modifiers = [
"add_provider_location_field",
"add_project_and_user_ids",
"add_metadata"
]
def summary_list(self, request, snapshots):
@ -74,6 +75,15 @@ class ViewBuilder(common.ViewBuilder):
snapshot_dict['user_id'] = snapshot.get('user_id')
snapshot_dict['project_id'] = snapshot.get('project_id')
@common.ViewBuilder.versioned_method("2.73")
def add_metadata(self, context, snapshot_dict, snapshot):
metadata = snapshot.get('share_snapshot_metadata')
if metadata:
metadata = {item['key']: item['value'] for item in metadata}
else:
metadata = {}
snapshot_dict['metadata'] = metadata
def _list_view(self, func, request, snapshots):
"""Provide a view for a list of share snapshots."""
snapshots_list = [func(request, snapshot)['snapshot']

View File

@ -626,9 +626,10 @@ def share_snapshot_create(context, values):
return IMPL.share_snapshot_create(context, values)
def share_snapshot_get(context, snapshot_id):
def share_snapshot_get(context, snapshot_id, project_only=True):
"""Get a snapshot or raise if it does not exist."""
return IMPL.share_snapshot_get(context, snapshot_id)
return IMPL.share_snapshot_get(context, snapshot_id,
project_only=project_only)
def share_snapshot_get_all(context, filters=None, limit=None, offset=None,
@ -761,7 +762,42 @@ def share_snapshot_instance_export_location_delete(context, el_id):
return IMPL.share_snapshot_instance_export_location_delete(context, el_id)
####################
def share_snapshot_metadata_get(context, share_snapshot_id, **kwargs):
"""Get all metadata for a share snapshot."""
return IMPL.share_snapshot_metadata_get(context,
share_snapshot_id,
**kwargs)
def share_snapshot_metadata_get_item(context, share_snapshot_id, key):
"""Get metadata item for a share snapshot."""
return IMPL.share_snapshot_metadata_get_item(context,
share_snapshot_id, key)
def share_snapshot_metadata_delete(context, share_snapshot_id, key):
"""Delete the given metadata item."""
IMPL.share_snapshot_metadata_delete(context, share_snapshot_id, key)
def share_snapshot_metadata_update(context, share_snapshot_id,
metadata, delete):
"""Update metadata if it exists, otherwise create it."""
return IMPL.share_snapshot_metadata_update(context, share_snapshot_id,
metadata, delete)
def share_snapshot_metadata_update_item(context, share_snapshot_id,
metadata):
"""Update metadata item if it exists, otherwise create it."""
return IMPL.share_snapshot_metadata_update_item(context,
share_snapshot_id,
metadata)
###################
def security_service_create(context, values):
"""Create security service DB record."""
return IMPL.security_service_create(context, values)

View File

@ -0,0 +1,66 @@
# 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.
"""add_snapshot_metadata_table
Revision ID: bb5938d74b73
Revises: a87e0fb17dee
Create Date: 2022-01-14 14:36:59.408638
"""
# revision identifiers, used by Alembic.
revision = 'bb5938d74b73'
down_revision = 'a87e0fb17dee'
from alembic import op
from oslo_log import log
import sqlalchemy as sql
LOG = log.getLogger(__name__)
share_snapshot_metadata_table_name = 'share_snapshot_metadata'
def upgrade():
context = op.get_context()
mysql_dl = context.bind.dialect.name == 'mysql'
datetime_type = (sql.dialects.mysql.DATETIME(fsp=6)
if mysql_dl else sql.DateTime)
try:
op.create_table(
share_snapshot_metadata_table_name,
sql.Column('deleted', sql.String(36), default='False'),
sql.Column('created_at', datetime_type),
sql.Column('updated_at', datetime_type),
sql.Column('deleted_at', datetime_type),
sql.Column('share_snapshot_id', sql.String(36),
sql.ForeignKey('share_snapshots.id'), nullable=False),
sql.Column('key', sql.String(255), nullable=False),
sql.Column('value', sql.String(1023), nullable=False),
sql.Column('id', sql.Integer, primary_key=True, nullable=False),
mysql_engine='InnoDB',
mysql_charset='utf8'
)
except Exception:
LOG.error("Table |%s| not created!",
share_snapshot_metadata_table_name)
raise
def downgrade():
try:
op.drop_table(share_snapshot_metadata_table_name)
except Exception:
LOG.error("Table |%s| not dropped!",
share_snapshot_metadata_table_name)
raise

View File

@ -39,6 +39,7 @@ from oslo_db.sqlalchemy import utils as db_utils
from oslo_log import log
from oslo_utils import excutils
from oslo_utils import importutils
from oslo_utils import strutils
from oslo_utils import timeutils
from oslo_utils import uuidutils
import sqlalchemy
@ -208,6 +209,20 @@ def require_share_exists(f):
return wrapper
def require_share_snapshot_exists(f):
"""Decorator to require the specified share snapshot to exist.
Requires the wrapped function to use context and share_snapshot_id as
their first two arguments.
"""
@wraps(f)
def wrapper(context, share_snapshot_id, *args, **kwargs):
share_snapshot_get(context, share_snapshot_id)
return f(context, share_snapshot_id, *args, **kwargs)
wrapper.__name__ = f.__name__
return wrapper
def require_share_instance_exists(f):
"""Decorator to require the specified share instance to exist.
@ -1999,10 +2014,10 @@ def share_replica_delete(context, share_replica_id, session=None,
@require_context
def _share_get_query(context, session=None):
def _share_get_query(context, session=None, **kwargs):
if session is None:
session = get_session()
return (model_query(context, models.Share, session=session).
return (model_query(context, models.Share, session=session, **kwargs).
options(joinedload('share_metadata')))
@ -2174,8 +2189,9 @@ def share_update(context, share_id, update_values):
@require_context
def share_get(context, share_id, session=None):
result = _share_get_query(context, session).filter_by(id=share_id).first()
def share_get(context, share_id, session=None, **kwargs):
result = _share_get_query(context, session, **kwargs).filter_by(
id=share_id).first()
if result is None:
raise exception.NotFound()
@ -2802,6 +2818,8 @@ def share_instance_access_update(context, access_id, instance_id, updates):
def share_snapshot_instance_create(context, snapshot_id, values, session=None):
session = session or get_session()
values = copy.deepcopy(values)
values['share_snapshot_metadata'] = _metadata_refs(
values.get('metadata'), models.ShareSnapshotMetadata)
_change_size_to_instance_size(values)
@ -2858,6 +2876,8 @@ def share_snapshot_instance_delete(context, snapshot_instance_id,
snapshot = share_snapshot_get(
context, snapshot_instance_ref['snapshot_id'], session=session)
if len(snapshot.instances) == 0:
session.query(models.ShareSnapshotMetadata).filter_by(
share_snapshot_id=snapshot['id']).soft_delete()
snapshot.soft_delete(session=session)
@ -2958,6 +2978,8 @@ def share_snapshot_create(context, create_values,
create_snapshot_instance=True):
values = copy.deepcopy(create_values)
values = ensure_model_dict_has_id(values)
values['share_snapshot_metadata'] = _metadata_refs(
values.pop('metadata', {}), models.ShareSnapshotMetadata)
snapshot_ref = models.ShareSnapshot()
snapshot_instance_values, snapshot_values = (
@ -3007,12 +3029,13 @@ def snapshot_data_get_for_project(context, project_id, user_id,
@require_context
def share_snapshot_get(context, snapshot_id, session=None):
def share_snapshot_get(context, snapshot_id, project_only=True, session=None):
result = (model_query(context, models.ShareSnapshot, session=session,
project_only=True).
project_only=project_only).
filter_by(id=snapshot_id).
options(joinedload('share')).
options(joinedload('instances')).
options(joinedload('share_snapshot_metadata')).
first())
if not result:
@ -3048,8 +3071,10 @@ def _share_snapshot_get_all_with_filters(context, project_id=None,
query = query.filter_by(project_id=project_id)
if share_id:
query = query.filter_by(share_id=share_id)
query = query.options(joinedload('share'))
query = query.options(joinedload('instances'))
query = (query.options(joinedload('share'))
.options(joinedload('instances'))
.options(joinedload('share_snapshot_metadata'))
)
# Snapshots with no instances are filtered out.
query = query.filter(
@ -3077,6 +3102,13 @@ def _share_snapshot_get_all_with_filters(context, project_id=None,
query = query.filter(models.ShareSnapshotInstance.status == (
filters['status']))
filters.pop('status')
if 'metadata' in filters:
for k, v in filters['metadata'].items():
# pylint: disable=no-member
query = query.filter(
or_(models.ShareSnapshot.share_snapshot_metadata.any(
key=k, value=v)))
filters.pop('metadata')
legal_filter_keys = ('display_name', 'display_name~',
'display_description', 'display_description~',
@ -3166,6 +3198,125 @@ def share_snapshot_instances_status_update(
return result
###################################
# Share Snapshot Metadata functions
###################################
@require_context
@require_share_snapshot_exists
def share_snapshot_metadata_get(context, share_snapshot_id):
session = get_session()
return _share_snapshot_metadata_get(context,
share_snapshot_id, session=session)
@require_context
@require_share_snapshot_exists
def share_snapshot_metadata_delete(context, share_snapshot_id, key):
session = get_session()
meta_ref = _share_snapshot_metadata_get_item(
context, share_snapshot_id, key, session=session)
meta_ref.soft_delete(session=session)
@require_context
@require_share_snapshot_exists
def share_snapshot_metadata_update(context, share_snapshot_id,
metadata, delete):
session = get_session()
return _share_snapshot_metadata_update(context, share_snapshot_id,
metadata, delete,
session=session)
def share_snapshot_metadata_update_item(context, share_snapshot_id,
item):
session = get_session()
return _share_snapshot_metadata_update(context, share_snapshot_id,
item, delete=False,
session=session)
def share_snapshot_metadata_get_item(context, share_snapshot_id,
key):
session = get_session()
row = _share_snapshot_metadata_get_item(context, share_snapshot_id,
key, session=session)
result = {}
result[row['key']] = row['value']
return result
def _share_snapshot_metadata_get_query(context, share_snapshot_id,
session=None):
session = session or get_session()
return (model_query(context, models.ShareSnapshotMetadata,
session=session,
read_deleted="no").
filter_by(share_snapshot_id=share_snapshot_id).
options(joinedload('share_snapshot')))
def _share_snapshot_metadata_get(context, share_snapshot_id, session=None):
session = session or get_session()
rows = _share_snapshot_metadata_get_query(context, share_snapshot_id,
session=session).all()
result = {}
for row in rows:
result[row['key']] = row['value']
return result
def _share_snapshot_metadata_get_item(context, share_snapshot_id,
key, session=None):
session = session or get_session()
result = (_share_snapshot_metadata_get_query(
context, share_snapshot_id, session=session).filter_by(
key=key).first())
if not result:
raise exception.MetadataItemNotFound
return result
@oslo_db_api.wrap_db_retry(max_retries=5, retry_on_deadlock=True)
def _share_snapshot_metadata_update(context, share_snapshot_id,
metadata, delete, session=None):
session = session or get_session()
delete = strutils.bool_from_string(delete)
with session.begin():
if delete:
original_metadata = _share_snapshot_metadata_get(
context, share_snapshot_id, session=session)
for meta_key, meta_value in original_metadata.items():
if meta_key not in metadata:
meta_ref = _share_snapshot_metadata_get_item(
context, share_snapshot_id, meta_key,
session=session)
meta_ref.soft_delete(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}
meta_ref = _share_snapshot_metadata_get_query(
context, share_snapshot_id,
session=session).filter_by(
key=meta_key).first()
if not meta_ref:
meta_ref = models.ShareSnapshotMetadata()
item.update({"key": meta_key,
"share_snapshot_id": share_snapshot_id})
meta_ref.update(item)
meta_ref.save(session=session)
return metadata
#################################
@ -3582,6 +3733,7 @@ def _share_metadata_update(context, share_id, metadata, delete, session=None):
with session.begin():
# Set existing metadata to deleted if delete argument is True
delete = strutils.bool_from_string(delete)
if delete:
original_metadata = _share_metadata_get(context, share_id,
session=session)

View File

@ -736,6 +736,24 @@ class ShareSnapshot(BASE, ManilaBase):
'ShareSnapshot.deleted == "False")')
class ShareSnapshotMetadata(BASE, ManilaBase):
"""Represents a metadata key/value pair for a snapshot."""
__tablename__ = 'share_snapshot_metadata'
id = Column(Integer, primary_key=True)
key = Column(String(255), nullable=False)
value = Column(String(1023), nullable=False)
deleted = Column(String(36), default='False')
share_snapshot_id = Column(String(36), ForeignKey(
'share_snapshots.id'), nullable=False)
share_snapshot = orm.relationship(
ShareSnapshot, backref="share_snapshot_metadata",
foreign_keys=share_snapshot_id,
primaryjoin='and_('
'ShareSnapshotMetadata.share_snapshot_id == ShareSnapshot.id,'
'ShareSnapshotMetadata.deleted == "False")')
class ShareSnapshotInstance(BASE, ManilaBase):
"""Represents a snapshot of a share."""
__tablename__ = 'share_snapshot_instances'

View File

@ -76,6 +76,24 @@ deprecated_snapshot_deny_access = policy.DeprecatedRule(
deprecated_reason=DEPRECATED_REASON,
deprecated_since=versionutils.deprecated.WALLABY
)
deprecated_update_snapshot_metadata = policy.DeprecatedRule(
name=BASE_POLICY_NAME % 'update_metadata',
check_str=base.RULE_DEFAULT,
deprecated_reason=DEPRECATED_REASON,
deprecated_since='ZED'
)
deprecated_delete_snapshot_metadata = policy.DeprecatedRule(
name=BASE_POLICY_NAME % 'delete_metadata',
check_str=base.RULE_DEFAULT,
deprecated_reason=DEPRECATED_REASON,
deprecated_since='ZED'
)
deprecated_get_snapshot_metadata = policy.DeprecatedRule(
name=BASE_POLICY_NAME % 'get_metadata',
check_str=base.RULE_DEFAULT,
deprecated_reason=DEPRECATED_REASON,
deprecated_since='ZED'
)
share_snapshot_policies = [
@ -208,6 +226,57 @@ share_snapshot_policies = [
],
deprecated_rule=deprecated_snapshot_deny_access
),
policy.DocumentedRuleDefault(
name=BASE_POLICY_NAME % 'update_metadata',
check_str=base.SYSTEM_ADMIN_OR_PROJECT_MEMBER,
scope_types=['system', 'project'],
description="Update snapshot metadata.",
operations=[
{
'method': 'PUT',
'path': '/snapshots/{snapshot_id}/metadata',
},
{
'method': 'POST',
'path': '/snapshots/{snapshot_id}/metadata/{key}',
},
{
'method': 'POST',
'path': '/snapshots/{snapshot_id}/metadata',
},
],
deprecated_rule=deprecated_update_snapshot_metadata
),
policy.DocumentedRuleDefault(
name=BASE_POLICY_NAME % 'delete_metadata',
check_str=base.SYSTEM_ADMIN_OR_PROJECT_MEMBER,
scope_types=['system', 'project'],
description="Delete snapshot metadata.",
operations=[
{
'method': 'DELETE',
'path': '/snapshots/{snapshot_id}/metadata/{key}',
}
],
deprecated_rule=deprecated_delete_snapshot_metadata
),
policy.DocumentedRuleDefault(
name=BASE_POLICY_NAME % 'get_metadata',
check_str=base.SYSTEM_OR_PROJECT_READER,
scope_types=['system', 'project'],
description="Get snapshot metadata.",
operations=[
{
'method': 'GET',
'path': '/snapshots/{snapshot_id}/metadata',
},
{
'method': 'GET',
'path': '/snapshots/{snapshot_id}/metadata/{key}',
}
],
deprecated_rule=deprecated_get_snapshot_metadata
),
]

View File

@ -1434,8 +1434,10 @@ class API(base.Base):
context, share_server, force=force)
def create_snapshot(self, context, share, name, description,
force=False):
force=False, metadata=None):
policy.check_policy(context, 'share', 'create_snapshot', share)
if metadata:
api_common._check_metadata_properties(metadata)
if ((not force) and (share['status'] != constants.STATUS_AVAILABLE)):
msg = _("Source share status must be "
@ -1486,6 +1488,8 @@ class API(base.Base):
'display_name': name,
'display_description': description,
'share_proto': share['share_proto']}
if metadata:
options.update({"metadata": metadata})
try:
snapshot = None
@ -2067,7 +2071,7 @@ class API(base.Base):
string_args = {'sort_key': sort_key, 'sort_dir': sort_dir}
string_args.update(search_opts)
for k, v in string_args.items():
if not (isinstance(v, str) and v):
if not (isinstance(v, str) and v) and k != 'metadata':
msg = _("Wrong '%(k)s' filter provided: "
"'%(v)s'.") % {'k': k, 'v': string_args[k]}
raise exception.InvalidInput(reason=msg)

View File

@ -118,6 +118,7 @@ def stub_snapshot(id, **kwargs):
'display_name': 'displaysnapname',
'display_description': 'displaysnapdesc',
'created_at': datetime.datetime(1, 1, 1, 1, 1, 1),
'metadata': {}
}
snapshot.update(kwargs)
return snapshot

View File

@ -15,6 +15,7 @@
from unittest import mock
import ast
import ddt
from oslo_serialization import jsonutils
import webob
@ -144,7 +145,7 @@ class ShareSnapshotAPITest(test.TestCase):
req,
200)
@ddt.data('2.0', '2.16', '2.17')
@ddt.data('2.0', '2.16', '2.17', '2.73')
def test_snapshot_show(self, version):
req = fakes.HTTPRequest.blank('/v2/fake/snapshots/200',
version=version)
@ -247,6 +248,46 @@ class ShareSnapshotAPITest(test.TestCase):
self._snapshot_list_summary_with_search_opts(
version=version, use_admin_context=use_admin_context)
def test_snapshot_list_metadata_filter(self, version='2.73',
use_admin_context=True):
search_opts = {
'sort_key': 'fake_sort_key',
'sort_dir': 'fake_sort_dir',
'offset': '1',
'limit': '1',
'metadata': "{'foo': 'bar'}"
}
# fake_key should be filtered for non-admin
url = '/v2/fake/snapshots?fake_key=fake_value'
for k, v in search_opts.items():
url = url + '&' + k + '=' + v
req = fakes.HTTPRequest.blank(
url, use_admin_context=use_admin_context, version=version)
snapshots = [
{'id': 'id1', 'metadata': {'foo': 'bar'}}
]
self.mock_object(share_api.API, 'get_all_snapshots',
mock.Mock(return_value=snapshots))
result = self.controller.index(req)
search_opts_expected = {
'metadata': ast.literal_eval(search_opts['metadata'])
}
if use_admin_context:
search_opts_expected.update({'fake_key': 'fake_value'})
share_api.API.get_all_snapshots.assert_called_once_with(
req.environ['manila.context'],
limit=int(search_opts['limit']),
offset=int(search_opts['offset']),
sort_key=search_opts['sort_key'],
sort_dir=search_opts['sort_dir'],
search_opts=search_opts_expected,
)
self.assertEqual(1, len(result['snapshots']))
self.assertEqual(snapshots[0]['id'], result['snapshots'][0]['id'])
def _snapshot_list_detail_with_search_opts(self, use_admin_context):
search_opts = fake_share.search_opts()
# fake_key should be filtered for non-admin

View File

@ -3175,3 +3175,81 @@ class ShareServerMultipleSubnets(BaseMigrationChecks):
).first()
self.test_case.assertFalse(
hasattr(na_record, 'share_network_subnet_id'))
@map_to_migration('bb5938d74b73')
class AddSnapshotMetadata(BaseMigrationChecks):
snapshot_id = uuidutils.generate_uuid()
new_table_name = 'share_snapshot_metadata'
def setup_upgrade_data(self, engine):
# Setup Share
share_data = {
'id': uuidutils.generate_uuid(),
'share_proto': "NFS",
'size': 1,
'snapshot_id': None,
'user_id': 'fake',
'project_id': 'fake'
}
share_table = utils.load_table('shares', engine)
engine.execute(share_table.insert(share_data))
share_instance_data = {
'id': uuidutils.generate_uuid(),
'deleted': 'False',
'host': 'fake',
'share_id': share_data['id'],
'status': 'available',
'access_rules_status': 'active',
'cast_rules_to_readonly': False,
}
share_instance_table = utils.load_table('share_instances', engine)
engine.execute(share_instance_table.insert(share_instance_data))
# Setup Share Snapshot
share_snapshot_data = {
'id': self.snapshot_id,
'share_id': share_data['id']
}
snapshot_table = utils.load_table('share_snapshots', engine)
engine.execute(snapshot_table.insert(share_snapshot_data))
# Setup snapshot instances
snapshot_instance_data = {
'id': uuidutils.generate_uuid(),
'snapshot_id': share_snapshot_data['id'],
'share_instance_id': share_instance_data['id']
}
snap_i_table = utils.load_table('share_snapshot_instances', engine)
engine.execute(snap_i_table.insert(snapshot_instance_data))
def check_upgrade(self, engine, data):
data = {
'id': 1,
'key': 't' * 255,
'value': 'v' * 1023,
'share_snapshot_id': self.snapshot_id,
'deleted': 'False',
}
new_table = utils.load_table(self.new_table_name, engine)
engine.execute(new_table.insert(data))
item = engine.execute(
new_table.select().where(new_table.c.id == data['id'])).first()
self.test_case.assertTrue(hasattr(item, 'id'))
self.test_case.assertEqual(data['id'], item['id'])
self.test_case.assertTrue(hasattr(item, 'key'))
self.test_case.assertEqual(data['key'], item['key'])
self.test_case.assertTrue(hasattr(item, 'value'))
self.test_case.assertEqual(data['value'], item['value'])
self.test_case.assertTrue(hasattr(item, 'share_snapshot_id'))
self.test_case.assertEqual(self.snapshot_id,
item['share_snapshot_id'])
self.test_case.assertTrue(hasattr(item, 'deleted'))
self.test_case.assertEqual('False', item['deleted'])
def check_downgrade(self, engine):
self.test_case.assertRaises(sa_exc.NoSuchTableError, utils.load_table,
self.new_table_name, engine)

View File

@ -1666,7 +1666,7 @@ class ShareSnapshotDatabaseAPITestCase(test.TestCase):
instances=self.snapshot_instances[0:3])
self.snapshot_2 = db_utils.create_snapshot(
id='fake_snapshot_id_2', share_id=self.share_2['id'],
instances=self.snapshot_instances[3:4])
instances=self.snapshot_instances[3:4], metadata={'foo': 'bar'})
self.snapshot_instance_export_locations = [
db_utils.create_snapshot_instance_export_locations(
@ -1711,14 +1711,21 @@ class ShareSnapshotDatabaseAPITestCase(test.TestCase):
def test_share_snapshot_get_all_with_filters_some(self):
expected_status = constants.STATUS_AVAILABLE
filters = {
'status': expected_status
'status': expected_status,
'metadata': {'foo': 'bar'}
}
snapshots = db_api.share_snapshot_get_all(
self.ctxt, filters=filters)
for snapshot in snapshots:
s = snapshot.get('share_snapshot_metadata')
for k, v in filters['metadata'].items():
filter_meta_key = k
filter_meta_val = v
self.assertEqual('fake_snapshot_id_2', snapshot['id'])
self.assertEqual(snapshot['status'], filters['status'])
self.assertEqual(s[0]['key'], filter_meta_key)
self.assertEqual(s[0]['value'], filter_meta_val)
self.assertEqual(1, len(snapshots))
@ -2044,6 +2051,68 @@ class ShareSnapshotDatabaseAPITestCase(test.TestCase):
db_api.share_snapshot_instance_export_locations_update,
self.ctxt, snapshot.instance['id'], new_export_locations, False)
def test_share_snapshot_metadata_get(self):
metadata = {'a': 'b', 'c': 'd'}
self.share_1 = db_utils.create_share(size=1)
self.snapshot_1 = db_utils.create_snapshot(
share_id=self.share_1['id'])
db_api.share_snapshot_metadata_update(
self.ctxt, share_snapshot_id=self.snapshot_1['id'],
metadata=metadata, delete=False)
self.assertEqual(
metadata, db_api.share_snapshot_metadata_get(
self.ctxt, share_snapshot_id=self.snapshot_1['id']))
def test_share_snapshot_metadata_get_item(self):
metadata = {'a': 'b', 'c': 'd'}
key = 'a'
shouldbe = {'a': 'b'}
self.share_1 = db_utils.create_share(size=1)
self.snapshot_1 = db_utils.create_snapshot(
share_id=self.share_1['id'])
db_api.share_snapshot_metadata_update(
self.ctxt, share_snapshot_id=self.snapshot_1['id'],
metadata=metadata, delete=False)
self.assertEqual(
shouldbe, db_api.share_snapshot_metadata_get_item(
self.ctxt, share_snapshot_id=self.snapshot_1['id'],
key=key))
def test_share_snapshot_metadata_update(self):
metadata1 = {'a': '1', 'c': '2'}
metadata2 = {'a': '3', 'd': '5'}
should_be = {'a': '3', 'c': '2', 'd': '5'}
self.share_1 = db_utils.create_share(size=1)
self.snapshot_1 = db_utils.create_snapshot(
share_id=self.share_1['id'])
db_api.share_snapshot_metadata_update(
self.ctxt, share_snapshot_id=self.snapshot_1['id'],
metadata=metadata1, delete=False)
db_api.share_snapshot_metadata_update(
self.ctxt, share_snapshot_id=self.snapshot_1['id'],
metadata=metadata2, delete=False)
self.assertEqual(
should_be, db_api.share_snapshot_metadata_get(
self.ctxt, share_snapshot_id=self.snapshot_1['id']))
def test_share_snapshot_metadata_delete(self):
key = 'a'
metadata = {'a': '1', 'c': '2'}
should_be = {'c': '2'}
self.share_1 = db_utils.create_share(size=1)
self.snapshot_1 = db_utils.create_snapshot(
share_id=self.share_1['id'])
db_api.share_snapshot_metadata_update(
self.ctxt, share_snapshot_id=self.snapshot_1['id'],
metadata=metadata, delete=False)
db_api.share_snapshot_metadata_delete(
self.ctxt, share_snapshot_id=self.snapshot_1['id'],
key=key)
self.assertEqual(
should_be, db_api.share_snapshot_metadata_get(
self.ctxt, share_snapshot_id=self.snapshot_1['id']))
class ShareExportLocationsDatabaseAPITestCase(test.TestCase):

View File

@ -207,7 +207,11 @@ def expected_snapshot(version=None, id='fake_snapshot_id', **kwargs):
'user_id': 'fakesnapuser',
'project_id': 'fakesnapproject',
})
if version and (api_version.APIVersionRequest(version)
>= api_version.APIVersionRequest('2.73')):
snapshot.update({
'metadata': {}
})
snapshot.update(kwargs)
return {'snapshot': snapshot}

View File

@ -0,0 +1,7 @@
---
features:
- |
Adds snapshot metadata capabilities inlcuding, create, update all,
update single, show, and delete metadata. Snapshots may be
filtered using metadata keys. Snapshot metadata is
available to admin and nonadmin users.