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:
parent
6b7bc9f37b
commit
206885a3e9
@ -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
|
||||
|
||||
|
||||
|
@ -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
|
||||
|
@ -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()))
|
||||
|
||||
|
@ -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}
|
||||
|
@ -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
|
||||
|
@ -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())
|
||||
|
@ -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']
|
||||
|
@ -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)
|
||||
|
@ -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
|
@ -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)
|
||||
|
@ -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'
|
||||
|
@ -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
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
||||
|
@ -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}
|
||||
|
||||
|
@ -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.
|
Loading…
Reference in New Issue
Block a user