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.
|
network.
|
||||||
* 2.71 - Added 'updated_at' field in share instance show API output.
|
* 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.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
|
# The default api version request is defined to be the
|
||||||
# minimum version of the API supported.
|
# minimum version of the API supported.
|
||||||
_MIN_API_VERSION = "2.0"
|
_MIN_API_VERSION = "2.0"
|
||||||
_MAX_API_VERSION = "2.72"
|
_MAX_API_VERSION = "2.73"
|
||||||
DEFAULT_API_VERSION = _MIN_API_VERSION
|
DEFAULT_API_VERSION = _MIN_API_VERSION
|
||||||
|
|
||||||
|
|
||||||
|
@ -399,4 +399,10 @@ ____
|
|||||||
|
|
||||||
2.72
|
2.72
|
||||||
----
|
----
|
||||||
|
|
||||||
Added 'share_network' option to share replica create API.
|
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."""
|
"""The share snapshots api."""
|
||||||
|
|
||||||
|
import ast
|
||||||
from http import client as http_client
|
from http import client as http_client
|
||||||
|
|
||||||
from oslo_log import log
|
from oslo_log import log
|
||||||
@ -22,6 +23,7 @@ import webob
|
|||||||
from webob import exc
|
from webob import exc
|
||||||
|
|
||||||
from manila.api import common
|
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.openstack import wsgi
|
||||||
from manila.api.views import share_snapshots as snapshot_views
|
from manila.api.views import share_snapshots as snapshot_views
|
||||||
from manila import db
|
from manila import db
|
||||||
@ -114,6 +116,18 @@ class ShareSnapshotMixin(object):
|
|||||||
search_opts['display_description'] = search_opts.pop(
|
search_opts['display_description'] = search_opts.pop(
|
||||||
'description')
|
'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
|
# like filter
|
||||||
for key, db_key in (('name~', 'display_name~'),
|
for key, db_key in (('name~', 'display_name~'),
|
||||||
('description~', 'display_description~')):
|
('description~', 'display_description~')):
|
||||||
@ -141,7 +155,7 @@ class ShareSnapshotMixin(object):
|
|||||||
def _get_snapshots_search_options(self):
|
def _get_snapshots_search_options(self):
|
||||||
"""Return share snapshot search options allowed by non-admin."""
|
"""Return share snapshot search options allowed by non-admin."""
|
||||||
return ('display_name', 'status', 'share_id', 'size', 'display_name~',
|
return ('display_name', 'status', 'share_id', 'size', 'display_name~',
|
||||||
'display_description~', 'display_description')
|
'display_description~', 'display_description', 'metadata')
|
||||||
|
|
||||||
def update(self, req, id, body):
|
def update(self, req, id, body):
|
||||||
"""Update a snapshot."""
|
"""Update a snapshot."""
|
||||||
@ -212,11 +226,20 @@ class ShareSnapshotMixin(object):
|
|||||||
snapshot['display_description'] = snapshot.get('description')
|
snapshot['display_description'] = snapshot.get('description')
|
||||||
del snapshot['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(
|
new_snapshot = self.share_api.create_snapshot(
|
||||||
context,
|
context,
|
||||||
share,
|
share,
|
||||||
snapshot.get('display_name'),
|
snapshot.get('display_name'),
|
||||||
snapshot.get('display_description'))
|
snapshot.get('display_description'),
|
||||||
|
**kwargs)
|
||||||
return self._view_builder.detail(
|
return self._view_builder.detail(
|
||||||
req, dict(new_snapshot.items()))
|
req, dict(new_snapshot.items()))
|
||||||
|
|
||||||
|
@ -26,30 +26,37 @@ class MetadataController(object):
|
|||||||
# From db, ensure it exists
|
# From db, ensure it exists
|
||||||
resource_get = {
|
resource_get = {
|
||||||
"share": "share_get",
|
"share": "share_get",
|
||||||
|
"share_snapshot": "share_snapshot_get",
|
||||||
}
|
}
|
||||||
|
|
||||||
resource_metadata_get = {
|
resource_metadata_get = {
|
||||||
"share": "share_metadata_get",
|
"share": "share_metadata_get",
|
||||||
|
"share_snapshot": "share_snapshot_metadata_get",
|
||||||
}
|
}
|
||||||
|
|
||||||
resource_metadata_get_item = {
|
resource_metadata_get_item = {
|
||||||
"share": "share_metadata_get_item",
|
"share": "share_metadata_get_item",
|
||||||
|
"share_snapshot": "share_snapshot_metadata_get_item",
|
||||||
}
|
}
|
||||||
|
|
||||||
resource_metadata_update = {
|
resource_metadata_update = {
|
||||||
"share": "share_metadata_update",
|
"share": "share_metadata_update",
|
||||||
|
"share_snapshot": "share_snapshot_metadata_update",
|
||||||
}
|
}
|
||||||
|
|
||||||
resource_metadata_update_item = {
|
resource_metadata_update_item = {
|
||||||
"share": "share_metadata_update_item",
|
"share": "share_metadata_update_item",
|
||||||
|
"share_snapshot": "share_snapshot_metadata_update_item",
|
||||||
}
|
}
|
||||||
|
|
||||||
resource_metadata_delete = {
|
resource_metadata_delete = {
|
||||||
"share": "share_metadata_delete",
|
"share": "share_metadata_delete",
|
||||||
|
"share_snapshot": "share_snapshot_metadata_delete",
|
||||||
}
|
}
|
||||||
|
|
||||||
resource_policy_get = {
|
resource_policy_get = {
|
||||||
'share': 'get',
|
'share': 'get',
|
||||||
|
'share_snapshot': 'get_snapshot',
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
@ -60,7 +67,8 @@ class MetadataController(object):
|
|||||||
for_modification=False, parent_id=None):
|
for_modification=False, parent_id=None):
|
||||||
if self.resource_name in ['share']:
|
if self.resource_name in ['share']:
|
||||||
# we would allow retrieving some "public" resources
|
# we would allow retrieving some "public" resources
|
||||||
# across project namespaces
|
# across project namespaces excpet share snaphots,
|
||||||
|
# project_only=True is hard coded
|
||||||
kwargs = {}
|
kwargs = {}
|
||||||
else:
|
else:
|
||||||
kwargs = {'project_only': True}
|
kwargs = {'project_only': True}
|
||||||
|
@ -255,6 +255,44 @@ class APIRouter(manila.api.openstack.APIRouter):
|
|||||||
controller=self.resources["snapshots"],
|
controller=self.resources["snapshots"],
|
||||||
collection={"detail": "GET"},
|
collection={"detail": "GET"},
|
||||||
member={"action": "POST"})
|
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}', '']:
|
for path_prefix in ['/{project_id}', '']:
|
||||||
# project_id is optional
|
# 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 api_version_request as api_version
|
||||||
from manila.api.openstack import wsgi
|
from manila.api.openstack import wsgi
|
||||||
from manila.api.v1 import share_snapshots
|
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.api.views import share_snapshots as snapshot_views
|
||||||
from manila.common import constants
|
from manila.common import constants
|
||||||
from manila.db import api as db_api
|
from manila.db import api as db_api
|
||||||
@ -37,7 +38,8 @@ LOG = log.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class ShareSnapshotsController(share_snapshots.ShareSnapshotMixin,
|
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."""
|
"""The Share Snapshots API V2 controller for the OpenStack API."""
|
||||||
|
|
||||||
resource_name = 'share_snapshot'
|
resource_name = 'share_snapshot'
|
||||||
@ -123,6 +125,12 @@ class ShareSnapshotsController(share_snapshots.ShareSnapshotMixin,
|
|||||||
'display_name': name,
|
'display_name': name,
|
||||||
'display_description': description,
|
'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:
|
try:
|
||||||
share_ref = self.share_api.get(context, share_id)
|
share_ref = self.share_api.get(context, share_id)
|
||||||
@ -339,6 +347,37 @@ class ShareSnapshotsController(share_snapshots.ShareSnapshotMixin,
|
|||||||
req.GET.pop('description', None)
|
req.GET.pop('description', None)
|
||||||
return self._get_snapshots(req, is_detail=True)
|
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():
|
def create_resource():
|
||||||
return wsgi.Resource(ShareSnapshotsController())
|
return wsgi.Resource(ShareSnapshotsController())
|
||||||
|
@ -23,6 +23,7 @@ class ViewBuilder(common.ViewBuilder):
|
|||||||
_detail_version_modifiers = [
|
_detail_version_modifiers = [
|
||||||
"add_provider_location_field",
|
"add_provider_location_field",
|
||||||
"add_project_and_user_ids",
|
"add_project_and_user_ids",
|
||||||
|
"add_metadata"
|
||||||
]
|
]
|
||||||
|
|
||||||
def summary_list(self, request, snapshots):
|
def summary_list(self, request, snapshots):
|
||||||
@ -74,6 +75,15 @@ class ViewBuilder(common.ViewBuilder):
|
|||||||
snapshot_dict['user_id'] = snapshot.get('user_id')
|
snapshot_dict['user_id'] = snapshot.get('user_id')
|
||||||
snapshot_dict['project_id'] = snapshot.get('project_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):
|
def _list_view(self, func, request, snapshots):
|
||||||
"""Provide a view for a list of share snapshots."""
|
"""Provide a view for a list of share snapshots."""
|
||||||
snapshots_list = [func(request, snapshot)['snapshot']
|
snapshots_list = [func(request, snapshot)['snapshot']
|
||||||
|
@ -626,9 +626,10 @@ def share_snapshot_create(context, values):
|
|||||||
return IMPL.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."""
|
"""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,
|
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)
|
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):
|
def security_service_create(context, values):
|
||||||
"""Create security service DB record."""
|
"""Create security service DB record."""
|
||||||
return IMPL.security_service_create(context, values)
|
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_log import log
|
||||||
from oslo_utils import excutils
|
from oslo_utils import excutils
|
||||||
from oslo_utils import importutils
|
from oslo_utils import importutils
|
||||||
|
from oslo_utils import strutils
|
||||||
from oslo_utils import timeutils
|
from oslo_utils import timeutils
|
||||||
from oslo_utils import uuidutils
|
from oslo_utils import uuidutils
|
||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
@ -208,6 +209,20 @@ def require_share_exists(f):
|
|||||||
return wrapper
|
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):
|
def require_share_instance_exists(f):
|
||||||
"""Decorator to require the specified share instance to exist.
|
"""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
|
@require_context
|
||||||
def _share_get_query(context, session=None):
|
def _share_get_query(context, session=None, **kwargs):
|
||||||
if session is None:
|
if session is None:
|
||||||
session = get_session()
|
session = get_session()
|
||||||
return (model_query(context, models.Share, session=session).
|
return (model_query(context, models.Share, session=session, **kwargs).
|
||||||
options(joinedload('share_metadata')))
|
options(joinedload('share_metadata')))
|
||||||
|
|
||||||
|
|
||||||
@ -2174,8 +2189,9 @@ def share_update(context, share_id, update_values):
|
|||||||
|
|
||||||
|
|
||||||
@require_context
|
@require_context
|
||||||
def share_get(context, share_id, session=None):
|
def share_get(context, share_id, session=None, **kwargs):
|
||||||
result = _share_get_query(context, session).filter_by(id=share_id).first()
|
result = _share_get_query(context, session, **kwargs).filter_by(
|
||||||
|
id=share_id).first()
|
||||||
|
|
||||||
if result is None:
|
if result is None:
|
||||||
raise exception.NotFound()
|
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):
|
def share_snapshot_instance_create(context, snapshot_id, values, session=None):
|
||||||
session = session or get_session()
|
session = session or get_session()
|
||||||
values = copy.deepcopy(values)
|
values = copy.deepcopy(values)
|
||||||
|
values['share_snapshot_metadata'] = _metadata_refs(
|
||||||
|
values.get('metadata'), models.ShareSnapshotMetadata)
|
||||||
|
|
||||||
_change_size_to_instance_size(values)
|
_change_size_to_instance_size(values)
|
||||||
|
|
||||||
@ -2858,6 +2876,8 @@ def share_snapshot_instance_delete(context, snapshot_instance_id,
|
|||||||
snapshot = share_snapshot_get(
|
snapshot = share_snapshot_get(
|
||||||
context, snapshot_instance_ref['snapshot_id'], session=session)
|
context, snapshot_instance_ref['snapshot_id'], session=session)
|
||||||
if len(snapshot.instances) == 0:
|
if len(snapshot.instances) == 0:
|
||||||
|
session.query(models.ShareSnapshotMetadata).filter_by(
|
||||||
|
share_snapshot_id=snapshot['id']).soft_delete()
|
||||||
snapshot.soft_delete(session=session)
|
snapshot.soft_delete(session=session)
|
||||||
|
|
||||||
|
|
||||||
@ -2958,6 +2978,8 @@ def share_snapshot_create(context, create_values,
|
|||||||
create_snapshot_instance=True):
|
create_snapshot_instance=True):
|
||||||
values = copy.deepcopy(create_values)
|
values = copy.deepcopy(create_values)
|
||||||
values = ensure_model_dict_has_id(values)
|
values = ensure_model_dict_has_id(values)
|
||||||
|
values['share_snapshot_metadata'] = _metadata_refs(
|
||||||
|
values.pop('metadata', {}), models.ShareSnapshotMetadata)
|
||||||
|
|
||||||
snapshot_ref = models.ShareSnapshot()
|
snapshot_ref = models.ShareSnapshot()
|
||||||
snapshot_instance_values, snapshot_values = (
|
snapshot_instance_values, snapshot_values = (
|
||||||
@ -3007,12 +3029,13 @@ def snapshot_data_get_for_project(context, project_id, user_id,
|
|||||||
|
|
||||||
|
|
||||||
@require_context
|
@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,
|
result = (model_query(context, models.ShareSnapshot, session=session,
|
||||||
project_only=True).
|
project_only=project_only).
|
||||||
filter_by(id=snapshot_id).
|
filter_by(id=snapshot_id).
|
||||||
options(joinedload('share')).
|
options(joinedload('share')).
|
||||||
options(joinedload('instances')).
|
options(joinedload('instances')).
|
||||||
|
options(joinedload('share_snapshot_metadata')).
|
||||||
first())
|
first())
|
||||||
|
|
||||||
if not result:
|
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)
|
query = query.filter_by(project_id=project_id)
|
||||||
if share_id:
|
if share_id:
|
||||||
query = query.filter_by(share_id=share_id)
|
query = query.filter_by(share_id=share_id)
|
||||||
query = query.options(joinedload('share'))
|
query = (query.options(joinedload('share'))
|
||||||
query = query.options(joinedload('instances'))
|
.options(joinedload('instances'))
|
||||||
|
.options(joinedload('share_snapshot_metadata'))
|
||||||
|
)
|
||||||
|
|
||||||
# Snapshots with no instances are filtered out.
|
# Snapshots with no instances are filtered out.
|
||||||
query = query.filter(
|
query = query.filter(
|
||||||
@ -3077,6 +3102,13 @@ def _share_snapshot_get_all_with_filters(context, project_id=None,
|
|||||||
query = query.filter(models.ShareSnapshotInstance.status == (
|
query = query.filter(models.ShareSnapshotInstance.status == (
|
||||||
filters['status']))
|
filters['status']))
|
||||||
filters.pop('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~',
|
legal_filter_keys = ('display_name', 'display_name~',
|
||||||
'display_description', 'display_description~',
|
'display_description', 'display_description~',
|
||||||
@ -3166,6 +3198,125 @@ def share_snapshot_instances_status_update(
|
|||||||
|
|
||||||
return result
|
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():
|
with session.begin():
|
||||||
# Set existing metadata to deleted if delete argument is True
|
# Set existing metadata to deleted if delete argument is True
|
||||||
|
delete = strutils.bool_from_string(delete)
|
||||||
if delete:
|
if delete:
|
||||||
original_metadata = _share_metadata_get(context, share_id,
|
original_metadata = _share_metadata_get(context, share_id,
|
||||||
session=session)
|
session=session)
|
||||||
|
@ -736,6 +736,24 @@ class ShareSnapshot(BASE, ManilaBase):
|
|||||||
'ShareSnapshot.deleted == "False")')
|
'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):
|
class ShareSnapshotInstance(BASE, ManilaBase):
|
||||||
"""Represents a snapshot of a share."""
|
"""Represents a snapshot of a share."""
|
||||||
__tablename__ = 'share_snapshot_instances'
|
__tablename__ = 'share_snapshot_instances'
|
||||||
|
@ -76,6 +76,24 @@ deprecated_snapshot_deny_access = policy.DeprecatedRule(
|
|||||||
deprecated_reason=DEPRECATED_REASON,
|
deprecated_reason=DEPRECATED_REASON,
|
||||||
deprecated_since=versionutils.deprecated.WALLABY
|
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 = [
|
share_snapshot_policies = [
|
||||||
@ -208,6 +226,57 @@ share_snapshot_policies = [
|
|||||||
],
|
],
|
||||||
deprecated_rule=deprecated_snapshot_deny_access
|
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)
|
context, share_server, force=force)
|
||||||
|
|
||||||
def create_snapshot(self, context, share, name, description,
|
def create_snapshot(self, context, share, name, description,
|
||||||
force=False):
|
force=False, metadata=None):
|
||||||
policy.check_policy(context, 'share', 'create_snapshot', share)
|
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)):
|
if ((not force) and (share['status'] != constants.STATUS_AVAILABLE)):
|
||||||
msg = _("Source share status must be "
|
msg = _("Source share status must be "
|
||||||
@ -1486,6 +1488,8 @@ class API(base.Base):
|
|||||||
'display_name': name,
|
'display_name': name,
|
||||||
'display_description': description,
|
'display_description': description,
|
||||||
'share_proto': share['share_proto']}
|
'share_proto': share['share_proto']}
|
||||||
|
if metadata:
|
||||||
|
options.update({"metadata": metadata})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
snapshot = None
|
snapshot = None
|
||||||
@ -2067,7 +2071,7 @@ class API(base.Base):
|
|||||||
string_args = {'sort_key': sort_key, 'sort_dir': sort_dir}
|
string_args = {'sort_key': sort_key, 'sort_dir': sort_dir}
|
||||||
string_args.update(search_opts)
|
string_args.update(search_opts)
|
||||||
for k, v in string_args.items():
|
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: "
|
msg = _("Wrong '%(k)s' filter provided: "
|
||||||
"'%(v)s'.") % {'k': k, 'v': string_args[k]}
|
"'%(v)s'.") % {'k': k, 'v': string_args[k]}
|
||||||
raise exception.InvalidInput(reason=msg)
|
raise exception.InvalidInput(reason=msg)
|
||||||
|
@ -118,6 +118,7 @@ def stub_snapshot(id, **kwargs):
|
|||||||
'display_name': 'displaysnapname',
|
'display_name': 'displaysnapname',
|
||||||
'display_description': 'displaysnapdesc',
|
'display_description': 'displaysnapdesc',
|
||||||
'created_at': datetime.datetime(1, 1, 1, 1, 1, 1),
|
'created_at': datetime.datetime(1, 1, 1, 1, 1, 1),
|
||||||
|
'metadata': {}
|
||||||
}
|
}
|
||||||
snapshot.update(kwargs)
|
snapshot.update(kwargs)
|
||||||
return snapshot
|
return snapshot
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
|
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|
||||||
|
import ast
|
||||||
import ddt
|
import ddt
|
||||||
from oslo_serialization import jsonutils
|
from oslo_serialization import jsonutils
|
||||||
import webob
|
import webob
|
||||||
@ -144,7 +145,7 @@ class ShareSnapshotAPITest(test.TestCase):
|
|||||||
req,
|
req,
|
||||||
200)
|
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):
|
def test_snapshot_show(self, version):
|
||||||
req = fakes.HTTPRequest.blank('/v2/fake/snapshots/200',
|
req = fakes.HTTPRequest.blank('/v2/fake/snapshots/200',
|
||||||
version=version)
|
version=version)
|
||||||
@ -247,6 +248,46 @@ class ShareSnapshotAPITest(test.TestCase):
|
|||||||
self._snapshot_list_summary_with_search_opts(
|
self._snapshot_list_summary_with_search_opts(
|
||||||
version=version, use_admin_context=use_admin_context)
|
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):
|
def _snapshot_list_detail_with_search_opts(self, use_admin_context):
|
||||||
search_opts = fake_share.search_opts()
|
search_opts = fake_share.search_opts()
|
||||||
# fake_key should be filtered for non-admin
|
# fake_key should be filtered for non-admin
|
||||||
|
@ -3175,3 +3175,81 @@ class ShareServerMultipleSubnets(BaseMigrationChecks):
|
|||||||
).first()
|
).first()
|
||||||
self.test_case.assertFalse(
|
self.test_case.assertFalse(
|
||||||
hasattr(na_record, 'share_network_subnet_id'))
|
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])
|
instances=self.snapshot_instances[0:3])
|
||||||
self.snapshot_2 = db_utils.create_snapshot(
|
self.snapshot_2 = db_utils.create_snapshot(
|
||||||
id='fake_snapshot_id_2', share_id=self.share_2['id'],
|
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 = [
|
self.snapshot_instance_export_locations = [
|
||||||
db_utils.create_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):
|
def test_share_snapshot_get_all_with_filters_some(self):
|
||||||
expected_status = constants.STATUS_AVAILABLE
|
expected_status = constants.STATUS_AVAILABLE
|
||||||
filters = {
|
filters = {
|
||||||
'status': expected_status
|
'status': expected_status,
|
||||||
|
'metadata': {'foo': 'bar'}
|
||||||
}
|
}
|
||||||
snapshots = db_api.share_snapshot_get_all(
|
snapshots = db_api.share_snapshot_get_all(
|
||||||
self.ctxt, filters=filters)
|
self.ctxt, filters=filters)
|
||||||
|
|
||||||
for snapshot in snapshots:
|
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('fake_snapshot_id_2', snapshot['id'])
|
||||||
self.assertEqual(snapshot['status'], filters['status'])
|
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))
|
self.assertEqual(1, len(snapshots))
|
||||||
|
|
||||||
@ -2044,6 +2051,68 @@ class ShareSnapshotDatabaseAPITestCase(test.TestCase):
|
|||||||
db_api.share_snapshot_instance_export_locations_update,
|
db_api.share_snapshot_instance_export_locations_update,
|
||||||
self.ctxt, snapshot.instance['id'], new_export_locations, False)
|
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):
|
class ShareExportLocationsDatabaseAPITestCase(test.TestCase):
|
||||||
|
|
||||||
|
@ -207,7 +207,11 @@ def expected_snapshot(version=None, id='fake_snapshot_id', **kwargs):
|
|||||||
'user_id': 'fakesnapuser',
|
'user_id': 'fakesnapuser',
|
||||||
'project_id': 'fakesnapproject',
|
'project_id': 'fakesnapproject',
|
||||||
})
|
})
|
||||||
|
if version and (api_version.APIVersionRequest(version)
|
||||||
|
>= api_version.APIVersionRequest('2.73')):
|
||||||
|
snapshot.update({
|
||||||
|
'metadata': {}
|
||||||
|
})
|
||||||
snapshot.update(kwargs)
|
snapshot.update(kwargs)
|
||||||
return {'snapshot': snapshot}
|
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