Manage and unmanage snapshot

Add APIs to support manage and unmanage share snapshots.
Also add support in the Generic driver.
This only supports for DHSS=False driver mode.

Add provider_location column to the share_snapshots table
to save data used to identify the snapshot on the backend.

Also need to bump microversion.

APIImpact
DocImpact
Change-Id: I87a066173c85d969607d132accd9f0e9bd49c235
Implements: blueprint manage-unmanage-snapshot
This commit is contained in:
Xing Yang 2015-11-22 16:10:44 -05:00
parent a9b6e2759e
commit c91f27f4e1
37 changed files with 2082 additions and 225 deletions

View File

@ -66,6 +66,7 @@ PASSWORD_FOR_SAMBA_USER=${PASSWORD_FOR_SAMBA_USER:-$USERNAME_FOR_USER_RULES}
RUN_MANILA_CG_TESTS=${RUN_MANILA_CG_TESTS:-True}
RUN_MANILA_MANAGE_TESTS=${RUN_MANILA_MANAGE_TESTS:-True}
RUN_MANILA_MANAGE_SNAPSHOT_TESTS=${RUN_MANILA_MANAGE_SNAPSHOT_TESTS:-False}
MANILA_CONF=${MANILA_CONF:-/etc/manila/manila.conf}
@ -128,6 +129,7 @@ if [[ "$TEST_TYPE" == "scenario" ]]; then
echo "Set test set to scenario only"
MANILA_TESTS='manila_tempest_tests.tests.scenario'
elif [[ "$DRIVER" == "generic" ]]; then
RUN_MANILA_MANAGE_SNAPSHOT_TESTS=True
if [[ "$POSTGRES_ENABLED" == "True" ]]; then
# Run only CIFS tests on PostgreSQL DB backend
# to reduce amount of tests per job using 'generic' share driver.
@ -165,6 +167,9 @@ iniset $TEMPEST_CONFIG share run_consistency_group_tests $RUN_MANILA_CG_TESTS
# Enable manage/unmanage tests
iniset $TEMPEST_CONFIG share run_manage_unmanage_tests $RUN_MANILA_MANAGE_TESTS
# Enable manage/unmanage snapshot tests
iniset $TEMPEST_CONFIG share run_manage_unmanage_snapshot_tests $RUN_MANILA_MANAGE_SNAPSHOT_TESTS
# Also, we should wait until service VM is available
# before running Tempest tests using Generic driver in DHSS=False mode.
source $BASE/new/manila/contrib/ci/common.sh

View File

@ -30,39 +30,39 @@ Column value "-" means that this feature is not currently supported.
Mapping of share drivers and share features support
---------------------------------------------------
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+
| Driver name | create/delete share | manage/unmanage share | extend share | shrink share | create/delete snapshot | create share from snapshot |
+========================================+=============================+=======================+==============+==============+========================+============================+
| Generic (Cinder as back-end) | DHSS = True (J) & False (K) | K | L | L | J | J |
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+
| NetApp Clustered Data ONTAP | DHSS = True (J) & False (K) | L | L | L | J | J |
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+
| EMC VNX | DHSS = True (J) | \- | \- | \- | J | J |
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+
| EMC Isilon | DHSS = False (K) | \- | M | \- | K | K |
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+
| Red Hat GlusterFS | DHSS = False (J) | \- | \- | \- | volume layout (L) | volume layout (L) |
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+
| Red Hat GlusterFS-Native | DHSS = False (J) | \- | \- | \- | K | L |
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+
| HDFS | DHSS = False (K) | \- | M | \- | K | K |
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+
| Hitachi HNAS | DHSS = False (L) | L | L | M | L | L |
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+
| HPE 3PAR | DHSS = True (L) & False (K) | \- | \- | \- | K | K |
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+
| Huawei | DHSS = True (M) & False(K) | L | L | L | K | M |
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+
| IBM GPFS | DHSS = False(K) | \- | L | \- | K | K |
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+
| LVM | DHSS = False (M) | \- | M | \- | M | M |
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+
| Quobyte | DHSS = False (K) | \- | M | M | \- | \- |
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+
| Windows SMB | DHSS = True (L) & False (L) | L | L | L | L | L |
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+
| Oracle ZFSSA | DHSS = False (K) | \- | \- | \- | K | K |
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
| Driver name | create/delete share | manage/unmanage share | extend share | shrink share | create/delete snapshot | create share from snapshot | manage/unmanage snapshot |
+========================================+=============================+=======================+==============+==============+========================+============================+==========================+
| Generic (Cinder as back-end) | DHSS = True (J) & False (K) | K | L | L | J | J | DHSS = False (M) |
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
| NetApp Clustered Data ONTAP | DHSS = True (J) & False (K) | L | L | L | J | J | \- |
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
| EMC VNX | DHSS = True (J) | \- | \- | \- | J | J | \- |
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
| EMC Isilon | DHSS = False (K) | \- | M | \- | K | K | \- |
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
| Red Hat GlusterFS | DHSS = False (J) | \- | \- | \- | volume layout (L) | volume layout (L) | \- |
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
| Red Hat GlusterFS-Native | DHSS = False (J) | \- | \- | \- | K | L | \- |
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
| HDFS | DHSS = False (K) | \- | M | \- | K | K | \- |
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
| Hitachi HNAS | DHSS = False (L) | L | L | M | L | L | \- |
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
| HPE 3PAR | DHSS = True (L) & False (K) | \- | \- | \- | K | K | \- |
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
| Huawei | DHSS = True (M) & False(K) | L | L | L | K | M | \- |
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
| IBM GPFS | DHSS = False(K) | \- | L | \- | K | K | \- |
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
| LVM | DHSS = False (M) | \- | M | \- | M | M | \- |
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
| Quobyte | DHSS = False (K) | \- | M | M | \- | \- | \- |
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
| Windows SMB | DHSS = True (L) & False (L) | L | L | L | L | L | \- |
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
| Oracle ZFSSA | DHSS = False (K) | \- | \- | \- | K | K | \- |
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
.. note::

View File

@ -52,6 +52,8 @@
"share_snapshot:get_snapshot": "rule:default",
"share_snapshot:get_all_snapshots": "rule:default",
"share_snapshot:snapshot_update": "rule:default",
"share_snapshot:manage_snapshot": "rule:admin_api",
"share_snapshot:unmanage_snapshot": "rule:admin_api",
"share_snapshot:force_delete": "rule:admin_api",
"share_snapshot:reset_status": "rule:admin_api",

View File

@ -58,14 +58,14 @@ REST_API_VERSION_HISTORY = """
* 2.10 - Field 'access_rules_status' was added to shares and share
instances.
* 2.11 - Share Replication support
* 2.12 - Manage/unmanage snapshot API.
"""
# The minimum and maximum versions of the API supported
# The default api version request is defined to be the
# the minimum version of the API supported.
_MIN_API_VERSION = "2.0"
_MAX_API_VERSION = "2.11"
_MAX_API_VERSION = "2.12"
DEFAULT_API_VERSION = _MIN_API_VERSION

View File

@ -85,3 +85,7 @@ user documentation.
'Experimental'. Share APIs return two new attributes: 'has_replicas' and
'replication_type'. Share instance APIs return a new attribute,
'replica_state'.
2.12
----
Share snapshot manage and unmanage API.

View File

@ -30,15 +30,8 @@ from manila import share
LOG = log.getLogger(__name__)
class ShareSnapshotsController(wsgi.Controller, wsgi.AdminActionsMixin):
"""The Share Snapshots API controller for the OpenStack API."""
resource_name = 'share_snapshot'
_view_builder_class = snapshot_views.ViewBuilder
def __init__(self):
super(ShareSnapshotsController, self).__init__()
self.share_api = share.API()
class ShareSnapshotMixin(object):
"""Mixin class for Share Snapshot Controllers."""
def _update(self, *args, **kwargs):
db.share_snapshot_update(*args, **kwargs)
@ -49,26 +42,6 @@ class ShareSnapshotsController(wsgi.Controller, wsgi.AdminActionsMixin):
def _delete(self, *args, **kwargs):
return self.share_api.delete_snapshot(*args, **kwargs)
@wsgi.Controller.api_version('1.0', '2.6')
@wsgi.action('os-reset_status')
def snapshot_reset_status_legacy(self, req, id, body):
return self._reset_status(req, id, body)
@wsgi.Controller.api_version('2.7')
@wsgi.action('reset_status')
def snapshot_reset_status(self, req, id, body):
return self._reset_status(req, id, body)
@wsgi.Controller.api_version('1.0', '2.6')
@wsgi.action('os-force_delete')
def snapshot_force_delete_legacy(self, req, id, body):
return self._force_delete(req, id, body)
@wsgi.Controller.api_version('2.7')
@wsgi.action('force_delete')
def snapshot_force_delete(self, req, id, body):
return self._force_delete(req, id, body)
def show(self, req, id):
"""Return data about the given snapshot."""
context = req.environ['manila.context']
@ -219,5 +192,25 @@ class ShareSnapshotsController(wsgi.Controller, wsgi.AdminActionsMixin):
req, dict(new_snapshot.items()))
class ShareSnapshotsController(ShareSnapshotMixin, wsgi.Controller,
wsgi.AdminActionsMixin):
"""The Share Snapshots API controller for the OpenStack API."""
resource_name = 'share_snapshot'
_view_builder_class = snapshot_views.ViewBuilder
def __init__(self):
super(ShareSnapshotsController, self).__init__()
self.share_api = share.API()
@wsgi.action('os-reset_status')
def snapshot_reset_status_legacy(self, req, id, body):
return self._reset_status(req, id, body)
@wsgi.action('os-force_delete')
def snapshot_force_delete_legacy(self, req, id, body):
return self._force_delete(req, id, body)
def create_resource():
return wsgi.Resource(ShareSnapshotsController())

View File

@ -29,7 +29,6 @@ from manila.api.v1 import share_manage
from manila.api.v1 import share_metadata
from manila.api.v1 import share_networks
from manila.api.v1 import share_servers
from manila.api.v1 import share_snapshots
from manila.api.v1 import share_types_extra_specs
from manila.api.v1 import share_unmanage
from manila.api.v2 import availability_zones
@ -42,6 +41,7 @@ from manila.api.v2 import share_export_locations
from manila.api.v2 import share_instance_export_locations
from manila.api.v2 import share_instances
from manila.api.v2 import share_replicas
from manila.api.v2 import share_snapshots
from manila.api.v2 import share_types
from manila.api.v2 import shares
from manila.api import versions
@ -199,6 +199,12 @@ class APIRouter(manila.api.openstack.APIRouter):
collection={"detail": "GET"},
member={"action": "POST"})
mapper.connect("snapshots",
"/{project_id}/snapshots/manage",
controller=self.resources["snapshots"],
action="manage",
conditions={"method": ["POST"]})
self.resources["share_metadata"] = share_metadata.create_resource()
share_metadata_controller = self.resources["share_metadata"]

View File

@ -0,0 +1,177 @@
# Copyright 2013 NetApp
# Copyright 2015 EMC Corporation.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""The share snapshots api."""
from oslo_log import log
import six
import webob
from webob import exc
from manila.api.openstack import wsgi
from manila.api.v1 import share_snapshots
from manila.api.views import share_snapshots as snapshot_views
from manila.common import constants
from manila import exception
from manila.i18n import _, _LI
from manila import share
LOG = log.getLogger(__name__)
class ShareSnapshotsController(share_snapshots.ShareSnapshotMixin,
wsgi.Controller, wsgi.AdminActionsMixin):
"""The Share Snapshots API V2 controller for the OpenStack API."""
resource_name = 'share_snapshot'
_view_builder_class = snapshot_views.ViewBuilder
def __init__(self):
super(ShareSnapshotsController, self).__init__()
self.share_api = share.API()
@wsgi.Controller.authorize('unmanage_snapshot')
def _unmanage(self, req, id, body=None):
"""Unmanage a share snapshot."""
context = req.environ['manila.context']
LOG.info(_LI("Unmanage share snapshot with id: %s."), id)
try:
snapshot = self.share_api.get_snapshot(context, id)
share = self.share_api.get(context, snapshot['share_id'])
if share.get('share_server_id'):
msg = _("Operation 'unmanage_snapshot' is not supported for "
"snapshots of shares that are created with share"
" servers (created with share-networks).")
raise exc.HTTPForbidden(explanation=msg)
elif snapshot['status'] in constants.TRANSITIONAL_STATUSES:
msg = _("Snapshot with transitional state cannot be "
"unmanaged. Snapshot '%(s_id)s' is in '%(state)s' "
"state.") % {'state': snapshot['status'],
's_id': snapshot['id']}
raise exc.HTTPForbidden(explanation=msg)
self.share_api.unmanage_snapshot(context, snapshot, share['host'])
except (exception.ShareSnapshotNotFound, exception.ShareNotFound) as e:
raise exc.HTTPNotFound(explanation=six.text_type(e))
return webob.Response(status_int=202)
@wsgi.Controller.authorize('manage_snapshot')
def _manage(self, req, body):
"""Instruct Manila to manage an existing snapshot.
Required HTTP Body:
{
"snapshot":
{
"share_id": <Manila share id>,
"provider_location": <A string parameter that identifies the
snapshot on the backend>
}
}
Optional elements in 'snapshot' are:
name A name for the new snapshot.
description A description for the new snapshot.
driver_options Driver specific dicts for the existing snapshot.
"""
context = req.environ['manila.context']
snapshot_data = self._validate_manage_parameters(context, body)
# NOTE(vponomaryov): compatibility actions are required between API and
# DB layers for 'name' and 'description' API params that are
# represented in DB as 'display_name' and 'display_description'
# appropriately.
name = snapshot_data.get('display_name',
snapshot_data.get('name'))
description = snapshot_data.get(
'display_description', snapshot_data.get('description'))
snapshot = {
'share_id': snapshot_data['share_id'],
'provider_location': snapshot_data['provider_location'],
'display_name': name,
'display_description': description,
}
driver_options = snapshot_data.get('driver_options', {})
try:
snapshot_ref = self.share_api.manage_snapshot(context, snapshot,
driver_options)
except (exception.ShareNotFound, exception.ShareSnapshotNotFound) as e:
raise exc.HTTPNotFound(explanation=six.text_type(e))
except exception.ManageInvalidShareSnapshot as e:
raise exc.HTTPConflict(explanation=six.text_type(e))
return self._view_builder.detail(req, snapshot_ref)
def _validate_manage_parameters(self, context, body):
if not (body and self.is_valid_body(body, 'snapshot')):
msg = _("Snapshot entity not found in request body.")
raise exc.HTTPUnprocessableEntity(explanation=msg)
required_parameters = ('share_id', 'provider_location')
data = body['snapshot']
for parameter in required_parameters:
if parameter not in data:
msg = _("Required parameter %s not found.") % parameter
raise exc.HTTPUnprocessableEntity(explanation=msg)
if not data.get(parameter):
msg = _("Required parameter %s is empty.") % parameter
raise exc.HTTPUnprocessableEntity(explanation=msg)
return data
@wsgi.Controller.api_version('2.0', '2.6')
@wsgi.action('os-reset_status')
def snapshot_reset_status_legacy(self, req, id, body):
return self._reset_status(req, id, body)
@wsgi.Controller.api_version('2.7')
@wsgi.action('reset_status')
def snapshot_reset_status(self, req, id, body):
return self._reset_status(req, id, body)
@wsgi.Controller.api_version('2.0', '2.6')
@wsgi.action('os-force_delete')
def snapshot_force_delete_legacy(self, req, id, body):
return self._force_delete(req, id, body)
@wsgi.Controller.api_version('2.7')
@wsgi.action('force_delete')
def snapshot_force_delete(self, req, id, body):
return self._force_delete(req, id, body)
@wsgi.Controller.api_version('2.12')
@wsgi.response(202)
def manage(self, req, body):
return self._manage(req, body)
@wsgi.Controller.api_version('2.12')
@wsgi.action('unmanage')
def unmanage(self, req, id, body=None):
return self._unmanage(req, id, body)
def create_resource():
return wsgi.Resource(ShareSnapshotsController())

View File

@ -20,6 +20,9 @@ class ViewBuilder(common.ViewBuilder):
"""Model a server API response as a python dictionary."""
_collection_name = 'snapshots'
_detail_version_modifiers = [
"add_provider_location_field",
]
def summary_list(self, request, snapshots):
"""Show a list of share snapshots without many details."""
@ -41,21 +44,31 @@ class ViewBuilder(common.ViewBuilder):
def detail(self, request, snapshot):
"""Detailed view of a single share snapshot."""
return {
'snapshot': {
'id': snapshot.get('id'),
'share_id': snapshot.get('share_id'),
'share_size': snapshot.get('share_size'),
'created_at': snapshot.get('created_at'),
'status': snapshot.get('status'),
'name': snapshot.get('display_name'),
'description': snapshot.get('display_description'),
'size': snapshot.get('size'),
'share_proto': snapshot.get('share_proto'),
'links': self._get_links(request, snapshot['id'])
}
snapshot_dict = {
'id': snapshot.get('id'),
'share_id': snapshot.get('share_id'),
'share_size': snapshot.get('share_size'),
'created_at': snapshot.get('created_at'),
'status': snapshot.get('status'),
'name': snapshot.get('display_name'),
'description': snapshot.get('display_description'),
'size': snapshot.get('size'),
'share_proto': snapshot.get('share_proto'),
'links': self._get_links(request, snapshot['id']),
}
# NOTE(xyang): Only retrieve provider_location for admin.
context = request.environ['manila.context']
if context.is_admin:
self.update_versioned_resource_dict(request, snapshot_dict,
snapshot)
return {'snapshot': snapshot_dict}
@common.ViewBuilder.versioned_method("2.12")
def add_provider_location_field(self, snapshot_dict, snapshot):
snapshot_dict['provider_location'] = snapshot.get('provider_location')
def _list_view(self, func, request, snapshots):
"""Provide a view for a list of share snapshots."""
snapshots_list = [func(request, snapshot)['snapshot']

View File

@ -0,0 +1,36 @@
# 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 provider_location to share_snapshot_instances
Revision ID: eb6d5544cbbd
Revises: 5155c7077f99
Create Date: 2016-02-12 22:25:39.594545
"""
# revision identifiers, used by Alembic.
revision = 'eb6d5544cbbd'
down_revision = '5155c7077f99'
from alembic import op
import sqlalchemy as sa
def upgrade():
op.add_column(
'share_snapshot_instances',
sa.Column('provider_location', sa.String(255), nullable=True))
def downgrade():
op.drop_column('share_snapshot_instances', 'provider_location')

View File

@ -1128,7 +1128,7 @@ def extract_share_instance_values(values):
def extract_snapshot_instance_values(values):
fields = ['status', 'progress']
fields = ['status', 'progress', 'provider_location']
return extract_instance_values(values, fields)

View File

@ -583,7 +583,8 @@ class ShareInstanceAccessMapping(BASE, ManilaBase):
class ShareSnapshot(BASE, ManilaBase):
"""Represents a snapshot of a share."""
__tablename__ = 'share_snapshots'
_extra_keys = ['name', 'share_name', 'status', 'progress']
_extra_keys = ['name', 'share_name', 'status', 'progress',
'provider_location']
@property
def name(self):
@ -603,6 +604,11 @@ class ShareSnapshot(BASE, ManilaBase):
if self.instance:
return self.instance.progress
@property
def provider_location(self):
if self.instance:
return self.instance.provider_location
@property
def instance(self):
if len(self.instances) > 0:
@ -664,6 +670,7 @@ class ShareSnapshotInstance(BASE, ManilaBase):
String(36), ForeignKey('share_instances.id'), nullable=False)
status = Column(String(255))
progress = Column(String(255))
provider_location = Column(String(255))
share_instance = orm.relationship(
ShareInstance, backref="snapshot_instances",
primaryjoin=(

View File

@ -426,6 +426,10 @@ class ExportLocationNotFound(NotFound):
message = _("Export location %(uuid)s could not be found.")
class ShareNotFound(NotFound):
message = _("Share %(share_id)s could not be found.")
class ShareSnapshotNotFound(NotFound):
message = _("Snapshot %(snapshot_id)s could not be found.")
@ -443,6 +447,16 @@ class InvalidShareSnapshot(Invalid):
message = _("Invalid share snapshot: %(reason)s.")
class ManageInvalidShareSnapshot(InvalidShareSnapshot):
message = _("Manage existing share snapshot failed due to "
"invalid share snapshot: %(reason)s.")
class UnmanageInvalidShareSnapshot(InvalidShareSnapshot):
message = _("Unmanage existing share snapshot failed due to "
"invalid share snapshot: %(reason)s.")
class ShareMetadataNotFound(NotFound):
message = _("Metadata item is not found.")

View File

@ -517,6 +517,53 @@ class API(base.Base):
# share server here, when manage/unmanage operations will be supported
# for driver_handles_share_servers=True mode
def manage_snapshot(self, context, snapshot_data, driver_options):
try:
share = self.db.share_get(context, snapshot_data['share_id'])
except exception.NotFound:
raise exception.ShareNotFound(share_id=snapshot_data['share_id'])
existing_snapshots = self.db.share_snapshot_get_all_for_share(
context, snapshot_data['share_id'])
for existing_snap in existing_snapshots:
for inst in existing_snap.get('instances'):
if (snapshot_data['provider_location'] ==
inst['provider_location']):
msg = _("A share snapshot %(share_snapshot_id)s is "
"already managed for provider location "
"%(provider_location)s.") % {
'share_snapshot_id': existing_snap['id'],
'provider_location':
snapshot_data['provider_location'],
}
raise exception.ManageInvalidShareSnapshot(
reason=msg)
snapshot_data.update({
'user_id': context.user_id,
'project_id': context.project_id,
'status': constants.STATUS_MANAGING,
'share_size': share['size'],
'progress': '0%',
'share_proto': share['share_proto']
})
snapshot = self.db.share_snapshot_create(context, snapshot_data)
self.share_rpcapi.manage_snapshot(context, snapshot, share['host'],
driver_options)
return snapshot
def unmanage_snapshot(self, context, snapshot, host):
update_data = {'status': constants.STATUS_UNMANAGING,
'terminated_at': timeutils.utcnow()}
snapshot_ref = self.db.share_snapshot_update(context,
snapshot['id'],
update_data)
self.share_rpcapi.unmanage_snapshot(context, snapshot_ref, host)
@policy.wrap_check_policy('share')
def delete(self, context, share, force=False):
"""Delete share."""

View File

@ -826,6 +826,40 @@ class ShareDriver(object):
UnmanageInvalidShare exception, specifying a reason for the failure.
"""
def manage_existing_snapshot(self, snapshot, driver_options):
"""Brings an existing snapshot under Manila management.
If provided snapshot is not valid, then raise a
ManageInvalidShareSnapshot exception, specifying a reason for
the failure.
:param snapshot: ShareSnapshotInstance model with ShareSnapshot data.
Example:
{'id': <instance id>, 'snapshot_id': < snapshot id>,
'provider_location': <location>, ......}
:param driver_options: Optional driver-specific options provided
by admin. Example:
{'key': 'value', ......}
:return: model_update dictionary with required key 'size',
which should contain size of the share snapshot.
"""
raise NotImplementedError()
def unmanage_snapshot(self, snapshot):
"""Removes the specified snapshot from Manila management.
Does not delete the underlying backend share snapshot.
For most drivers, this will not need to do anything. However, some
drivers might use this call as an opportunity to clean up any
Manila-specific configuration that they have associated with the
backend share snapshot.
If provided share snapshot cannot be unmanaged, then raise an
UnmanageInvalidShareSnapshot exception, specifying a reason for
the failure.
"""
def extend_share(self, share, new_size, share_server=None):
"""Extends size of existing share.

View File

@ -745,6 +745,7 @@ class GenericShareDriver(driver.ExecuteMixin, driver.ShareDriver):
def create_snapshot(self, context, snapshot, share_server=None):
"""Creates a snapshot."""
model_update = {}
volume = self._get_volume(self.admin_context, snapshot['share_id'])
volume_snapshot_name = (self.configuration.
volume_snapshot_name_template % snapshot['id'])
@ -762,14 +763,22 @@ class GenericShareDriver(driver.ExecuteMixin, driver.ShareDriver):
self.admin_context,
volume_snapshot['id'])
# NOTE(xyang): We should look at whether we still need to save
# volume_snapshot_id in private_storage later, now that is saved
# in provider_location.
self.private_storage.update(
snapshot['id'], {'volume_snapshot_id': volume_snapshot['id']})
# NOTE(xyang): Need to update provider_location in the db so
# that it can be used in manage/unmanage snapshot tempest tests.
model_update['provider_location'] = volume_snapshot['id']
else:
raise exception.ManilaException(
_('Volume snapshot have not been '
'created in %ss. Giving up') %
self.configuration.max_time_to_create_volume)
return model_update
def delete_snapshot(self, context, snapshot, share_server=None):
"""Deletes a snapshot."""
volume_snapshot = self._get_volume_snapshot(self.admin_context,
@ -935,6 +944,46 @@ class GenericShareDriver(driver.ExecuteMixin, driver.ShareDriver):
server_details, old_export_location)
return {'size': share_size, 'export_locations': export_locations}
def manage_existing_snapshot(self, snapshot, driver_options):
"""Manage existing share snapshot with manila.
:param snapshot: Snapshot data
:param driver_options: Not used by the Generic driver currently
:return: dict with share snapshot size, example: {'size': 1}
"""
model_update = {}
volume_snapshot = None
snapshot_size = snapshot.get('share_size', 0)
provider_location = snapshot.get('provider_location')
try:
volume_snapshot = self.volume_api.get_snapshot(
self.admin_context,
provider_location)
except exception.VolumeSnapshotNotFound as e:
raise exception.ManageInvalidShareSnapshot(
reason=six.text_type(e))
if volume_snapshot:
snapshot_size = volume_snapshot['size']
# NOTE(xyang): volume_snapshot_id is saved in private_storage
# in create_snapshot, so saving it here too for consistency.
# We should look at whether we still need to save it in
# private_storage later.
self.private_storage.update(
snapshot['id'], {'volume_snapshot_id': volume_snapshot['id']})
# NOTE(xyang): provider_location is used to map a Manila snapshot
# to its name on the storage backend and prevent managing of the
# same snapshot twice.
model_update['provider_location'] = volume_snapshot['id']
model_update['size'] = snapshot_size
return model_update
def unmanage_snapshot(self, snapshot):
"""Unmanage share snapshot with manila."""
self.private_storage.delete(snapshot['id'])
def _get_mount_stats_by_index(self, mount_path, server_details, index,
block_size='G'):
"""Get mount stats using df shell command.

View File

@ -164,7 +164,7 @@ def add_hooks(f):
class ShareManager(manager.SchedulerDependentManager):
"""Manages NAS storages."""
RPC_API_VERSION = '1.8'
RPC_API_VERSION = '1.9'
def __init__(self, share_driver=None, service_name=None, *args, **kwargs):
"""Load the driver from args, or from flags."""
@ -1308,6 +1308,80 @@ class ShareManager(manager.SchedulerDependentManager):
{'status': constants.STATUS_MANAGE_ERROR, 'size': 1})
raise
@add_hooks
@utils.require_driver_initialized
def manage_snapshot(self, context, snapshot_id, driver_options):
if self.driver.driver_handles_share_servers:
msg = _("Manage snapshot is not supported for "
"driver_handles_share_servers=True mode.")
# NOTE(vponomaryov): set size as 1 because design expects size
# to be set, it also will allow us to handle delete/unmanage
# operations properly with this errored snapshot according to
# quotas.
self.db.share_snapshot_update(
context, snapshot_id,
{'status': constants.STATUS_MANAGE_ERROR, 'size': 1})
raise exception.InvalidDriverMode(driver_mode=msg)
context = context.elevated()
snapshot_ref = self.db.share_snapshot_get(context, snapshot_id)
share_server = self._get_share_server(context,
snapshot_ref['share'])
if share_server:
msg = _("Manage snapshot is not supported for "
"share snapshots with share servers.")
# NOTE(vponomaryov): set size as 1 because design expects size
# to be set, it also will allow us to handle delete/unmanage
# operations properly with this errored snapshot according to
# quotas.
self.db.share_snapshot_update(
context, snapshot_id,
{'status': constants.STATUS_MANAGE_ERROR, 'size': 1})
raise exception.InvalidShareSnapshot(reason=msg)
snapshot_instance = self.db.share_snapshot_instance_get(
context, snapshot_ref.instance['id'], with_share_data=True
)
project_id = snapshot_ref['project_id']
try:
snapshot_update = (
self.driver.manage_existing_snapshot(
snapshot_instance,
driver_options)
or {}
)
if not snapshot_update.get('size'):
snapshot_update['size'] = snapshot_ref['share']['size']
LOG.warning(_LI("Cannot get the size of the snapshot "
"%(snapshot_id)s. Using the size of "
"the share instead."),
{'snapshot_id': snapshot_id})
self._update_quota_usages(context, project_id, {
"snapshots": 1,
"snapshot_gigabytes": snapshot_update['size'],
})
snapshot_update.update({
'status': constants.STATUS_AVAILABLE,
'progress': '100%',
})
snapshot_update.pop('id', None)
self.db.share_snapshot_update(context, snapshot_id,
snapshot_update)
except Exception:
# NOTE(vponomaryov): set size as 1 because design expects size
# to be set, it also will allow us to handle delete/unmanage
# operations properly with this errored snapshot according to
# quotas.
self.db.share_snapshot_update(
context, snapshot_id,
{'status': constants.STATUS_MANAGE_ERROR, 'size': 1})
raise
def _update_quota_usages(self, context, project_id, usages):
user_id = context.user_id
for resource, usage in usages.items():
@ -1383,6 +1457,60 @@ class ShareManager(manager.SchedulerDependentManager):
self.db.share_instance_delete(context, share_instance['id'])
LOG.info(_LI("Share %s: unmanaged successfully."), share_id)
@add_hooks
@utils.require_driver_initialized
def unmanage_snapshot(self, context, snapshot_id):
status = {'status': constants.STATUS_UNMANAGE_ERROR}
if self.driver.driver_handles_share_servers:
msg = _("Unmanage snapshot is not supported for "
"driver_handles_share_servers=True mode.")
self.db.share_snapshot_update(context, snapshot_id, status)
LOG.error(_LE("Share snapshot cannot be unmanaged: %s."),
msg)
return
context = context.elevated()
snapshot_ref = self.db.share_snapshot_get(context, snapshot_id)
share_server = self._get_share_server(context,
snapshot_ref['share'])
snapshot_instance = self.db.share_snapshot_instance_get(
context, snapshot_ref.instance['id'], with_share_data=True
)
project_id = snapshot_ref['project_id']
if share_server:
msg = _("Unmanage snapshot is not supported for "
"share snapshots with share servers.")
self.db.share_snapshot_update(context, snapshot_id, status)
LOG.error(_LE("Share snapshot cannot be unmanaged: %s."),
msg)
return
try:
self.driver.unmanage_snapshot(snapshot_instance)
except exception.UnmanageInvalidShareSnapshot as e:
self.db.share_snapshot_update(context, snapshot_id, status)
LOG.error(_LE("Share snapshot cannot be unmanaged: %s."), e)
return
try:
reservations = QUOTAS.reserve(
context,
project_id=project_id,
snapshots=-1,
snapshot_gigabytes=-snapshot_ref['size'])
QUOTAS.commit(context, reservations, project_id=project_id)
except Exception as e:
# Note(imalinovskiy):
# Quota reservation errors here are not fatal, because
# unmanage is administrator API and he/she could update user
# quota usages later if it's required.
LOG.warning(_LW("Failed to update quota usages: %s."), e)
self.db.share_snapshot_destroy(context, snapshot_id)
@add_hooks
@utils.require_driver_initialized
def delete_share_instance(self, context, share_instance_id):
@ -1451,10 +1579,8 @@ class ShareManager(manager.SchedulerDependentManager):
context, snapshot_instance, share_server=share_server)
if model_update:
model_dict = model_update.to_dict()
self.db.share_snapshot_instance_update(
context, snapshot_instance_id, model_dict)
context, snapshot_instance_id, model_update)
except Exception:
with excutils.save_and_reraise_exception():
self.db.share_snapshot_instance_update(

View File

@ -51,6 +51,7 @@ class ShareAPI(object):
delete_share_replica()
promote_share_replica()
update_share_replica()
1.9 - Add manage_snapshot() and unmanage_snapshot() methods
"""
BASE_RPC_API_VERSION = '1.0'
@ -59,7 +60,7 @@ class ShareAPI(object):
super(ShareAPI, self).__init__()
target = messaging.Target(topic=CONF.share_topic,
version=self.BASE_RPC_API_VERSION)
self.client = rpc.get_client(target, version_cap='1.8')
self.client = rpc.get_client(target, version_cap='1.9')
def create_share_instance(self, context, share_instance, host,
request_spec, filter_properties,
@ -87,6 +88,22 @@ class ShareAPI(object):
call_context = self.client.prepare(server=host, version='1.1')
call_context.cast(context, 'unmanage_share', share_id=share['id'])
def manage_snapshot(self, context, snapshot, host,
driver_options=None):
new_host = utils.extract_host(host)
call_context = self.client.prepare(server=new_host, version='1.9')
call_context.cast(context,
'manage_snapshot',
snapshot_id=snapshot['id'],
driver_options=driver_options)
def unmanage_snapshot(self, context, snapshot, host):
new_host = utils.extract_host(host)
call_context = self.client.prepare(server=new_host, version='1.9')
call_context.cast(context,
'unmanage_snapshot',
snapshot_id=snapshot['id'])
def delete_share_instance(self, context, share_instance):
host = utils.extract_host(share_instance['host'])
call_context = self.client.prepare(server=host, version='1.4')

View File

@ -179,6 +179,18 @@ def app():
mapper['/v2'] = router_v2.APIRouter()
return mapper
fixture_reset_status_with_different_roles_v1 = (
{
'role': 'admin',
'valid_code': 202,
'valid_status': constants.STATUS_ERROR,
},
{
'role': 'member',
'valid_code': 403,
'valid_status': constants.STATUS_AVAILABLE,
},
)
fixture_reset_status_with_different_roles = (
{

View File

@ -13,8 +13,6 @@
# License for the specific language governing permissions and limitations
# under the License.
import datetime
import ddt
import mock
from oslo_serialization import jsonutils
@ -31,6 +29,7 @@ from manila import test
from manila.tests.api.contrib import stubs
from manila.tests.api import fakes
from manila.tests import db_utils
from manila.tests import fake_share
@ddt.ddt
@ -77,39 +76,18 @@ class ShareSnapshotAPITest(test.TestCase):
stubs.stub_snapshot_create)
body = {
'snapshot': {
'share_id': 100,
'share_id': 'fakeshareid',
'force': False,
'name': 'fake_share_name',
'description': 'fake_share_description',
'name': 'displaysnapname',
'description': 'displaysnapdesc',
}
}
req = fakes.HTTPRequest.blank('/snapshots')
res_dict = self.controller.create(req, body)
expected = {
'snapshot': {
'id': 200,
'share_id': 100,
'share_size': 1,
'created_at': datetime.datetime(1, 1, 1, 1, 1, 1),
'status': 'fakesnapstatus',
'name': 'fake_share_name',
'size': 1,
'description': 'fake_share_description',
'share_proto': 'fakesnapproto',
'links': [
{
'href': 'http://localhost/v1/fake/snapshots/200',
'rel': 'self',
},
{
'href': 'http://localhost/fake/snapshots/200',
'rel': 'bookmark',
},
],
}
}
expected = fake_share.expected_snapshot(id=200)
self.assertEqual(expected, res_dict)
@ddt.data(0, False)
@ -162,29 +140,7 @@ class ShareSnapshotAPITest(test.TestCase):
def test_snapshot_show(self):
req = fakes.HTTPRequest.blank('/snapshots/200')
res_dict = self.controller.show(req, 200)
expected = {
'snapshot': {
'id': 200,
'share_id': 'fakeshareid',
'share_size': 1,
'created_at': datetime.datetime(1, 1, 1, 1, 1, 1),
'status': 'fakesnapstatus',
'name': 'displaysnapname',
'size': 1,
'description': 'displaysnapdesc',
'share_proto': 'fakesnapproto',
'links': [
{
'href': 'http://localhost/v1/fake/snapshots/200',
'rel': 'self',
},
{
'href': 'http://localhost/fake/snapshots/200',
'rel': 'bookmark',
},
],
}
}
expected = fake_share.expected_snapshot(id=200)
self.assertEqual(expected, res_dict)
def test_snapshot_show_nofound(self):
@ -222,15 +178,7 @@ class ShareSnapshotAPITest(test.TestCase):
self.assertEqual(expected, res_dict)
def _snapshot_list_summary_with_search_opts(self, use_admin_context):
search_opts = {
'name': 'fake_name',
'status': 'fake_status',
'share_id': 'fake_share_id',
'sort_key': 'fake_sort_key',
'sort_dir': 'fake_sort_dir',
'offset': '1',
'limit': '1',
}
search_opts = fake_share.search_opts()
# fake_key should be filtered for non-admin
url = '/snapshots?fake_key=fake_value'
for k, v in search_opts.items():
@ -275,15 +223,7 @@ class ShareSnapshotAPITest(test.TestCase):
self._snapshot_list_summary_with_search_opts(use_admin_context=True)
def _snapshot_list_detail_with_search_opts(self, use_admin_context):
search_opts = {
'name': 'fake_name',
'status': 'fake_status',
'share_id': 'fake_share_id',
'sort_key': 'fake_sort_key',
'sort_dir': 'fake_sort_dir',
'limit': '1',
'offset': '1',
}
search_opts = fake_share.search_opts()
# fake_key should be filtered for non-admin
url = '/shares/detail?fake_key=fake_value'
for k, v in search_opts.items():
@ -348,32 +288,8 @@ class ShareSnapshotAPITest(test.TestCase):
env = {'QUERY_STRING': 'name=Share+Test+Name'}
req = fakes.HTTPRequest.blank('/shares/detail', environ=env)
res_dict = self.controller.detail(req)
expected = {
'snapshots': [
{
'id': 2,
'share_id': 'fakeshareid',
'share_size': 1,
'size': 1,
'created_at': datetime.datetime(1, 1, 1, 1, 1, 1),
'status': 'fakesnapstatus',
'name': 'displaysnapname',
'description': 'displaysnapdesc',
'share_proto': 'fakesnapproto',
'links': [
{
'href': 'http://localhost/v1/fake/snapshots/'
'2',
'rel': 'self',
},
{
'href': 'http://localhost/fake/snapshots/2',
'rel': 'bookmark',
},
],
},
]
}
expected_s = fake_share.expected_snapshot(id=2)
expected = {'snapshots': [expected_s['snapshot']]}
self.assertEqual(expected, res_dict)
def test_snapshot_list_status_none(self):
@ -443,26 +359,22 @@ class ShareSnapshotAdminActionsAPITest(test.TestCase):
def _get_context(self, role):
return getattr(self, '%s_context' % role)
def _setup_snapshot_data(self, snapshot=None, version='2.7'):
def _setup_snapshot_data(self, snapshot=None):
if snapshot is None:
share = db_utils.create_share()
snapshot = db_utils.create_snapshot(
status=constants.STATUS_AVAILABLE, share_id=share['id'])
req = fakes.HTTPRequest.blank('/v2/fake/snapshots/%s/action' %
snapshot['id'], version=version)
req = fakes.HTTPRequest.blank('/v1/fake/snapshots/%s/action' %
snapshot['id'])
return snapshot, req
def _reset_status(self, ctxt, model, req, db_access_method,
valid_code, valid_status=None, body=None, version='2.7'):
if float(version) > 2.6:
action_name = 'reset_status'
else:
action_name = 'os-reset_status'
valid_code, valid_status=None, body=None):
action_name = 'os-reset_status'
if body is None:
body = {action_name: {'status': constants.STATUS_ERROR}}
req.method = 'POST'
req.headers['content-type'] = 'application/json'
req.headers['X-Openstack-Manila-Api-Version'] = version
req.body = six.b(jsonutils.dumps(body))
req.environ['manila.context'] = ctxt
@ -480,39 +392,31 @@ class ShareSnapshotAdminActionsAPITest(test.TestCase):
actual_model = db_access_method(ctxt, model['id'])
self.assertEqual(valid_status, actual_model['status'])
@ddt.data(*fakes.fixture_reset_status_with_different_roles)
@ddt.data(*fakes.fixture_reset_status_with_different_roles_v1)
@ddt.unpack
def test_snapshot_reset_status_with_different_roles(self, role, valid_code,
valid_status, version):
valid_status):
ctxt = self._get_context(role)
snapshot, req = self._setup_snapshot_data(version=version)
snapshot, req = self._setup_snapshot_data()
self._reset_status(ctxt, snapshot, req, db.share_snapshot_get,
valid_code, valid_status, version=version)
valid_code, valid_status)
@ddt.data(
({'os-reset_status': {'x-status': 'bad'}}, '2.6'),
({'reset_status': {'x-status': 'bad'}}, '2.7'),
({'os-reset_status': {'status': 'invalid'}}, '2.6'),
({'reset_status': {'status': 'invalid'}}, '2.7'),
{'os-reset_status': {'x-status': 'bad'}},
{'os-reset_status': {'status': 'invalid'}},
)
@ddt.unpack
def test_snapshot_invalid_reset_status_body(self, body, version):
snapshot, req = self._setup_snapshot_data(version=version)
def test_snapshot_invalid_reset_status_body(self, body):
snapshot, req = self._setup_snapshot_data()
self._reset_status(self.admin_context, snapshot, req,
db.share_snapshot_get, 400,
constants.STATUS_AVAILABLE, body, version=version)
constants.STATUS_AVAILABLE, body)
def _force_delete(self, ctxt, model, req, db_access_method, valid_code,
version='2.7'):
if float(version) > 2.6:
action_name = 'force_delete'
else:
action_name = 'os-force_delete'
def _force_delete(self, ctxt, model, req, db_access_method, valid_code):
action_name = 'os-force_delete'
req.method = 'POST'
req.headers['content-type'] = 'application/json'
req.headers['X-Openstack-Manila-Api-Version'] = version
req.body = six.b(jsonutils.dumps({action_name: {}}))
req.environ['manila.context'] = ctxt
@ -521,15 +425,17 @@ class ShareSnapshotAdminActionsAPITest(test.TestCase):
# Validate response
self.assertEqual(valid_code, resp.status_int)
@ddt.data(*fakes.fixture_force_delete_with_different_roles)
@ddt.data(
{'role': 'admin', 'resp_code': 202},
{'role': 'member', 'resp_code': 403},
)
@ddt.unpack
def test_snapshot_force_delete_with_different_roles(self, role, resp_code,
version):
def test_snapshot_force_delete_with_different_roles(self, role, resp_code):
ctxt = self._get_context(role)
snapshot, req = self._setup_snapshot_data(version=version)
snapshot, req = self._setup_snapshot_data()
self._force_delete(ctxt, snapshot, req, db.share_snapshot_get,
resp_code, version=version)
resp_code)
def test_snapshot_force_delete_missing(self):
ctxt = self._get_context('admin')

View File

@ -0,0 +1,629 @@
# Copyright 2015 EMC Corporation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import ddt
import mock
from oslo_serialization import jsonutils
import six
import webob
from manila.api.v2 import share_snapshots
from manila.common import constants
from manila import context
from manila import db
from manila import exception
from manila import policy
from manila.share import api as share_api
from manila import test
from manila.tests.api.contrib import stubs
from manila.tests.api import fakes
from manila.tests import db_utils
from manila.tests import fake_share
MIN_MANAGE_SNAPSHOT_API_VERSION = '2.12'
def get_fake_manage_body(share_id=None, provider_location=None,
driver_options=None, **kwargs):
fake_snapshot = {
'share_id': share_id,
'provider_location': provider_location,
'driver_options': driver_options,
}
fake_snapshot.update(kwargs)
return {'snapshot': fake_snapshot}
@ddt.ddt
class ShareSnapshotAPITest(test.TestCase):
"""Share Snapshot API Test."""
def setUp(self):
super(self.__class__, self).setUp()
self.controller = share_snapshots.ShareSnapshotsController()
self.mock_object(share_api.API, 'get', stubs.stub_share_get)
self.mock_object(share_api.API, 'get_all_snapshots',
stubs.stub_snapshot_get_all_by_project)
self.mock_object(share_api.API, 'get_snapshot',
stubs.stub_snapshot_get)
self.mock_object(share_api.API, 'snapshot_update',
stubs.stub_snapshot_update)
self.snp_example = {
'share_id': 100,
'size': 12,
'force': False,
'display_name': 'updated_snapshot_name',
'display_description': 'updated_snapshot_description',
}
def test_snapshot_create(self):
self.mock_object(share_api.API, 'create_snapshot',
stubs.stub_snapshot_create)
body = {
'snapshot': {
'share_id': 'fakeshareid',
'force': False,
'name': 'displaysnapname',
'description': 'displaysnapdesc',
}
}
req = fakes.HTTPRequest.blank('/snapshots')
res_dict = self.controller.create(req, body)
expected = fake_share.expected_snapshot(id=200)
self.assertEqual(expected, res_dict)
@ddt.data(0, False)
def test_snapshot_create_no_support(self, snapshot_support):
self.mock_object(share_api.API, 'create_snapshot')
self.mock_object(
share_api.API,
'get',
mock.Mock(return_value={'snapshot_support': snapshot_support}))
body = {
'snapshot': {
'share_id': 100,
'force': False,
'name': 'fake_share_name',
'description': 'fake_share_description',
}
}
req = fakes.HTTPRequest.blank('/snapshots')
self.assertRaises(
webob.exc.HTTPUnprocessableEntity,
self.controller.create, req, body)
self.assertFalse(share_api.API.create_snapshot.called)
def test_snapshot_create_no_body(self):
body = {}
req = fakes.HTTPRequest.blank('/snapshots')
self.assertRaises(webob.exc.HTTPUnprocessableEntity,
self.controller.create,
req,
body)
def test_snapshot_delete(self):
self.mock_object(share_api.API, 'delete_snapshot',
stubs.stub_snapshot_delete)
req = fakes.HTTPRequest.blank('/snapshots/200')
resp = self.controller.delete(req, 200)
self.assertEqual(202, resp.status_int)
def test_snapshot_delete_nofound(self):
self.mock_object(share_api.API, 'get_snapshot',
stubs.stub_snapshot_get_notfound)
req = fakes.HTTPRequest.blank('/snapshots/200')
self.assertRaises(webob.exc.HTTPNotFound,
self.controller.delete,
req,
200)
def test_snapshot_show(self):
req = fakes.HTTPRequest.blank('/snapshots/200')
res_dict = self.controller.show(req, 200)
expected = fake_share.expected_snapshot(id=200)
self.assertEqual(expected, res_dict)
def test_snapshot_show_nofound(self):
self.mock_object(share_api.API, 'get_snapshot',
stubs.stub_snapshot_get_notfound)
req = fakes.HTTPRequest.blank('/snapshots/200')
self.assertRaises(webob.exc.HTTPNotFound,
self.controller.show,
req, '200')
def test_snapshot_list_summary(self):
self.mock_object(share_api.API, 'get_all_snapshots',
stubs.stub_snapshot_get_all_by_project)
req = fakes.HTTPRequest.blank('/snapshots')
res_dict = self.controller.index(req)
expected = {
'snapshots': [
{
'name': 'displaysnapname',
'id': 2,
'links': [
{
'href': 'http://localhost/v1/fake/'
'snapshots/2',
'rel': 'self'
},
{
'href': 'http://localhost/fake/snapshots/2',
'rel': 'bookmark'
}
],
}
]
}
self.assertEqual(expected, res_dict)
def _snapshot_list_summary_with_search_opts(self, use_admin_context):
search_opts = fake_share.search_opts()
# fake_key should be filtered for non-admin
url = '/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)
snapshots = [
{'id': 'id1', 'display_name': 'n1', 'status': 'fake_status', },
{'id': 'id2', 'display_name': 'n2', 'status': 'fake_status', },
{'id': 'id3', 'display_name': 'n3', 'status': 'fake_status', },
]
self.mock_object(share_api.API, 'get_all_snapshots',
mock.Mock(return_value=snapshots))
result = self.controller.index(req)
search_opts_expected = {
'display_name': search_opts['name'],
'status': search_opts['status'],
'share_id': search_opts['share_id'],
}
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'],
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[1]['id'], result['snapshots'][0]['id'])
self.assertEqual(
snapshots[1]['display_name'], result['snapshots'][0]['name'])
def test_snapshot_list_summary_with_search_opts_by_non_admin(self):
self._snapshot_list_summary_with_search_opts(use_admin_context=False)
def test_snapshot_list_summary_with_search_opts_by_admin(self):
self._snapshot_list_summary_with_search_opts(use_admin_context=True)
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
url = '/shares/detail?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)
snapshots = [
{'id': 'id1', 'display_name': 'n1', 'status': 'fake_status', },
{
'id': 'id2',
'display_name': 'n2',
'status': 'fake_status',
'share_id': 'fake_share_id',
},
{'id': 'id3', 'display_name': 'n3', 'status': 'fake_status', },
]
self.mock_object(share_api.API, 'get_all_snapshots',
mock.Mock(return_value=snapshots))
result = self.controller.detail(req)
search_opts_expected = {
'display_name': search_opts['name'],
'status': search_opts['status'],
'share_id': search_opts['share_id'],
}
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'],
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[1]['id'], result['snapshots'][0]['id'])
self.assertEqual(
snapshots[1]['display_name'], result['snapshots'][0]['name'])
self.assertEqual(
snapshots[1]['status'], result['snapshots'][0]['status'])
self.assertEqual(
snapshots[1]['share_id'], result['snapshots'][0]['share_id'])
def test_share_list_detail_with_search_opts_by_non_admin(self):
self._snapshot_list_detail_with_search_opts(use_admin_context=False)
def test_share_list_detail_with_search_opts_by_admin(self):
self._snapshot_list_detail_with_search_opts(use_admin_context=True)
def test_snapshot_list_detail(self):
env = {'QUERY_STRING': 'name=Share+Test+Name'}
req = fakes.HTTPRequest.blank('/shares/detail', environ=env)
res_dict = self.controller.detail(req)
expected_s = fake_share.expected_snapshot(id=2)
expected = {'snapshots': [expected_s['snapshot']]}
self.assertEqual(expected, res_dict)
def test_snapshot_updates_description(self):
snp = self.snp_example
body = {"snapshot": snp}
req = fakes.HTTPRequest.blank('/snapshot/1')
res_dict = self.controller.update(req, 1, body)
self.assertEqual(snp["display_name"], res_dict['snapshot']["name"])
def test_snapshot_updates_display_descr(self):
snp = self.snp_example
body = {"snapshot": snp}
req = fakes.HTTPRequest.blank('/snapshot/1')
res_dict = self.controller.update(req, 1, body)
self.assertEqual(snp["display_description"],
res_dict['snapshot']["description"])
def test_share_not_updates_size(self):
snp = self.snp_example
body = {"snapshot": snp}
req = fakes.HTTPRequest.blank('/snapshot/1')
res_dict = self.controller.update(req, 1, body)
self.assertNotEqual(snp["size"], res_dict['snapshot']["size"])
@ddt.ddt
class ShareSnapshotAdminActionsAPITest(test.TestCase):
def setUp(self):
super(self.__class__, self).setUp()
self.controller = share_snapshots.ShareSnapshotsController()
self.flags(rpc_backend='manila.openstack.common.rpc.impl_fake')
self.admin_context = context.RequestContext('admin', 'fake', True)
self.member_context = context.RequestContext('fake', 'fake')
self.resource_name = self.controller.resource_name
self.manage_request = fakes.HTTPRequest.blank(
'/snapshots/manage', use_admin_context=True,
version=MIN_MANAGE_SNAPSHOT_API_VERSION)
self.snapshot_id = 'fake'
self.unmanage_request = fakes.HTTPRequest.blank(
'/snapshots/%s/unmanage' % self.snapshot_id,
use_admin_context=True,
version=MIN_MANAGE_SNAPSHOT_API_VERSION)
def _get_context(self, role):
return getattr(self, '%s_context' % role)
def _setup_snapshot_data(self, snapshot=None, version='2.7'):
if snapshot is None:
share = db_utils.create_share()
snapshot = db_utils.create_snapshot(
status=constants.STATUS_AVAILABLE, share_id=share['id'])
req = fakes.HTTPRequest.blank('/v2/fake/snapshots/%s/action' %
snapshot['id'], version=version)
return snapshot, req
def _reset_status(self, ctxt, model, req, db_access_method,
valid_code, valid_status=None, body=None, version='2.7'):
if float(version) > 2.6:
action_name = 'reset_status'
else:
action_name = 'os-reset_status'
if body is None:
body = {action_name: {'status': constants.STATUS_ERROR}}
req.method = 'POST'
req.headers['content-type'] = 'application/json'
req.headers['X-Openstack-Manila-Api-Version'] = version
req.body = six.b(jsonutils.dumps(body))
req.environ['manila.context'] = ctxt
resp = req.get_response(fakes.app())
# validate response code and model status
self.assertEqual(valid_code, resp.status_int)
if valid_code == 404:
self.assertRaises(exception.NotFound,
db_access_method,
ctxt,
model['id'])
else:
actual_model = db_access_method(ctxt, model['id'])
self.assertEqual(valid_status, actual_model['status'])
@ddt.data(*fakes.fixture_reset_status_with_different_roles)
@ddt.unpack
def test_snapshot_reset_status_with_different_roles(self, role, valid_code,
valid_status, version):
ctxt = self._get_context(role)
snapshot, req = self._setup_snapshot_data(version=version)
self._reset_status(ctxt, snapshot, req, db.share_snapshot_get,
valid_code, valid_status, version=version)
@ddt.data(
({'os-reset_status': {'x-status': 'bad'}}, '2.6'),
({'reset_status': {'x-status': 'bad'}}, '2.7'),
({'os-reset_status': {'status': 'invalid'}}, '2.6'),
({'reset_status': {'status': 'invalid'}}, '2.7'),
)
@ddt.unpack
def test_snapshot_invalid_reset_status_body(self, body, version):
snapshot, req = self._setup_snapshot_data(version=version)
self._reset_status(self.admin_context, snapshot, req,
db.share_snapshot_get, 400,
constants.STATUS_AVAILABLE, body, version=version)
def _force_delete(self, ctxt, model, req, db_access_method, valid_code,
version='2.7'):
if float(version) > 2.6:
action_name = 'force_delete'
else:
action_name = 'os-force_delete'
req.method = 'POST'
req.headers['content-type'] = 'application/json'
req.headers['X-Openstack-Manila-Api-Version'] = version
req.body = six.b(jsonutils.dumps({action_name: {}}))
req.environ['manila.context'] = ctxt
resp = req.get_response(fakes.app())
# Validate response
self.assertEqual(valid_code, resp.status_int)
@ddt.data(*fakes.fixture_force_delete_with_different_roles)
@ddt.unpack
def test_snapshot_force_delete_with_different_roles(self, role, resp_code,
version):
ctxt = self._get_context(role)
snapshot, req = self._setup_snapshot_data(version=version)
self._force_delete(ctxt, snapshot, req, db.share_snapshot_get,
resp_code, version=version)
def test_snapshot_force_delete_missing(self):
ctxt = self._get_context('admin')
snapshot, req = self._setup_snapshot_data(snapshot={'id': 'fake'})
self._force_delete(ctxt, snapshot, req, db.share_snapshot_get, 404)
@ddt.data(
{},
{'snapshots': {}},
{'snapshot': get_fake_manage_body(share_id='xxxxxxxx')},
{'snapshot': get_fake_manage_body(provider_location='xxxxxxxx')}
)
def test_snapshot_manage_invalid_body(self, body):
self.mock_policy_check = self.mock_object(
policy, 'check_policy', mock.Mock(return_value=True))
self.assertRaises(webob.exc.HTTPUnprocessableEntity,
self.controller.manage,
self.manage_request,
body)
self.mock_policy_check.assert_called_once_with(
self.manage_request.environ['manila.context'],
self.resource_name, 'manage_snapshot')
@ddt.data(
get_fake_manage_body(name='foo', description='bar'),
get_fake_manage_body(display_name='foo', description='bar'),
get_fake_manage_body(name='foo', display_description='bar'),
get_fake_manage_body(display_name='foo', display_description='bar'),
get_fake_manage_body(display_name='foo', display_description='bar'),
)
def test_snapshot_manage(self, data):
self.mock_policy_check = self.mock_object(
policy, 'check_policy', mock.Mock(return_value=True))
data['snapshot']['share_id'] = 'fake'
data['snapshot']['provider_location'] = 'fake_volume_snapshot_id'
data['snapshot']['driver_options'] = {}
return_snapshot = {'id': 'fake_snap'}
self.mock_object(
share_api.API, 'manage_snapshot', mock.Mock(
return_value=return_snapshot))
share_snapshot = {
'share_id': 'fake',
'provider_location': 'fake_volume_snapshot_id',
'display_name': 'foo',
'display_description': 'bar',
}
actual_result = self.controller.manage(self.manage_request, data)
share_api.API.manage_snapshot.assert_called_once_with(
mock.ANY, share_snapshot, data['snapshot']['driver_options'])
self.assertEqual(return_snapshot['id'],
actual_result['snapshot']['id'])
self.mock_policy_check.assert_called_once_with(
self.manage_request.environ['manila.context'],
self.resource_name, 'manage_snapshot')
@ddt.data(exception.ShareNotFound(share_id='fake'),
exception.ShareSnapshotNotFound(snapshot_id='fake'),
exception.ManageInvalidShareSnapshot(reason='error'))
def test_manage_exception(self, exception_type):
self.mock_policy_check = self.mock_object(
policy, 'check_policy', mock.Mock(return_value=True))
body = get_fake_manage_body(
share_id='fake', provider_location='fake_volume_snapshot_id',
driver_options={})
self.mock_object(
share_api.API, 'manage_snapshot', mock.Mock(
side_effect=exception_type))
if isinstance(exception_type, exception.ManageInvalidShareSnapshot):
http_ex = webob.exc.HTTPConflict
else:
http_ex = webob.exc.HTTPNotFound
self.assertRaises(http_ex,
self.controller.manage,
self.manage_request, body)
self.mock_policy_check.assert_called_once_with(
self.manage_request.environ['manila.context'],
self.resource_name, 'manage_snapshot')
@ddt.data('1.0', '2.6', '2.11')
def test_manage_version_not_found(self, version):
body = get_fake_manage_body(
share_id='fake', provider_location='fake_volume_snapshot_id',
driver_options={})
fake_req = fakes.HTTPRequest.blank(
'/snapshots/manage', use_admin_context=True,
version=version)
self.assertRaises(exception.VersionNotFoundForAPIMethod,
self.controller.manage,
fake_req, body)
def test_snapshot_unmanage_share_server(self):
self.mock_policy_check = self.mock_object(
policy, 'check_policy', mock.Mock(return_value=True))
share = {'status': constants.STATUS_AVAILABLE, 'id': 'bar_id',
'share_server_id': 'fake_server_id'}
self.mock_object(share_api.API, 'get', mock.Mock(return_value=share))
snapshot = {'status': constants.STATUS_AVAILABLE, 'id': 'foo_id',
'share_id': 'bar_id'}
self.mock_object(share_api.API, 'get_snapshot',
mock.Mock(return_value=snapshot))
self.assertRaises(webob.exc.HTTPForbidden,
self.controller.unmanage,
self.unmanage_request,
snapshot['id'])
self.controller.share_api.get_snapshot.assert_called_once_with(
self.unmanage_request.environ['manila.context'], snapshot['id'])
self.controller.share_api.get.assert_called_once_with(
self.unmanage_request.environ['manila.context'], share['id'])
self.mock_policy_check.assert_called_once_with(
self.unmanage_request.environ['manila.context'],
self.resource_name, 'unmanage_snapshot')
@ddt.data(*constants.TRANSITIONAL_STATUSES)
def test_snapshot_unmanage_with_transitional_state(self, status):
self.mock_policy_check = self.mock_object(
policy, 'check_policy', mock.Mock(return_value=True))
share = {'status': constants.STATUS_AVAILABLE, 'id': 'bar_id'}
self.mock_object(share_api.API, 'get', mock.Mock(return_value=share))
snapshot = {'status': status, 'id': 'foo_id', 'share_id': 'bar_id'}
self.mock_object(
self.controller.share_api, 'get_snapshot',
mock.Mock(return_value=snapshot))
self.assertRaises(
webob.exc.HTTPForbidden,
self.controller.unmanage, self.unmanage_request, snapshot['id'])
self.controller.share_api.get_snapshot.assert_called_once_with(
self.unmanage_request.environ['manila.context'], snapshot['id'])
self.controller.share_api.get.assert_called_once_with(
self.unmanage_request.environ['manila.context'], share['id'])
self.mock_policy_check.assert_called_once_with(
self.unmanage_request.environ['manila.context'],
self.resource_name, 'unmanage_snapshot')
def test_snapshot_unmanage(self):
self.mock_policy_check = self.mock_object(
policy, 'check_policy', mock.Mock(return_value=True))
share = {'status': constants.STATUS_AVAILABLE, 'id': 'bar_id',
'host': 'fake_host'}
self.mock_object(share_api.API, 'get', mock.Mock(return_value=share))
snapshot = {'status': constants.STATUS_AVAILABLE, 'id': 'foo_id',
'share_id': 'bar_id'}
self.mock_object(share_api.API, 'get_snapshot',
mock.Mock(return_value=snapshot))
self.mock_object(share_api.API, 'unmanage_snapshot', mock.Mock())
actual_result = self.controller.unmanage(self.unmanage_request,
snapshot['id'])
self.assertEqual(202, actual_result.status_int)
self.controller.share_api.get_snapshot.assert_called_once_with(
self.unmanage_request.environ['manila.context'], snapshot['id'])
share_api.API.unmanage_snapshot.assert_called_once_with(
mock.ANY, snapshot, 'fake_host')
self.mock_policy_check.assert_called_once_with(
self.unmanage_request.environ['manila.context'],
self.resource_name, 'unmanage_snapshot')
def test_unmanage_share_not_found(self):
self.mock_policy_check = self.mock_object(
policy, 'check_policy', mock.Mock(return_value=True))
self.mock_object(
share_api.API, 'get', mock.Mock(
side_effect=exception.ShareNotFound(share_id='fake')))
snapshot = {'status': constants.STATUS_AVAILABLE, 'id': 'foo_id',
'share_id': 'bar_id'}
self.mock_object(share_api.API, 'get_snapshot',
mock.Mock(return_value=snapshot))
self.mock_object(share_api.API, 'unmanage_snapshot', mock.Mock())
self.assertRaises(webob.exc.HTTPNotFound,
self.controller.unmanage,
self.unmanage_request, 'foo_id')
self.mock_policy_check.assert_called_once_with(
self.unmanage_request.environ['manila.context'],
self.resource_name, 'unmanage_snapshot')
def test_unmanage_snapshot_not_found(self):
self.mock_policy_check = self.mock_object(
policy, 'check_policy', mock.Mock(return_value=True))
share = {'status': constants.STATUS_AVAILABLE, 'id': 'bar_id'}
self.mock_object(share_api.API, 'get', mock.Mock(return_value=share))
self.mock_object(
share_api.API, 'get_snapshot', mock.Mock(
side_effect=exception.ShareSnapshotNotFound(
snapshot_id='foo_id')))
self.mock_object(share_api.API, 'unmanage_snapshot', mock.Mock())
self.assertRaises(webob.exc.HTTPNotFound,
self.controller.unmanage,
self.unmanage_request, 'foo_id')
self.mock_policy_check.assert_called_once_with(
self.unmanage_request.environ['manila.context'],
self.resource_name, 'unmanage_snapshot')
@ddt.data('1.0', '2.6', '2.11')
def test_unmanage_version_not_found(self, version):
snapshot_id = 'fake'
fake_req = fakes.HTTPRequest.blank(
'/snapshots/%s/unmanage' % snapshot_id,
use_admin_context=True,
version=version)
self.assertRaises(exception.VersionNotFoundForAPIMethod,
self.controller.unmanage,
fake_req, 'fake')

View File

@ -552,3 +552,57 @@ class NetworkAllocationsNewLabelColumnChecks(BaseMigrationChecks):
for col_name in ('label', 'network_type', 'segmentation_id',
'ip_version', 'cidr'):
self.test_case.assertFalse(hasattr(na, col_name))
@map_to_migration('eb6d5544cbbd')
class ShareSnapshotInstanceNewProviderLocationColumnChecks(
BaseMigrationChecks):
table_name = 'share_snapshot_instances'
def setup_upgrade_data(self, engine):
# Setup shares
share_data = {'id': 'new_share_id'}
s_table = utils.load_table('shares', engine)
engine.execute(s_table.insert(share_data))
# Setup share instances
share_instance_data = {
'id': 'new_share_instance_id',
'share_id': share_data['id']
}
si_table = utils.load_table('share_instances', engine)
engine.execute(si_table.insert(share_instance_data))
# Setup share snapshots
share_snapshot_data = {
'id': 'new_snapshot_id',
'share_id': share_data['id']}
snap_table = utils.load_table('share_snapshots', engine)
engine.execute(snap_table.insert(share_snapshot_data))
# Setup snapshot instances
snapshot_instance_data = {
'id': 'new_snapshot_instance_id',
'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):
ss_table = utils.load_table(self.table_name, engine)
db_result = engine.execute(ss_table.select())
self.test_case.assertTrue(db_result.rowcount > 0)
for ss in db_result:
self.test_case.assertTrue(hasattr(ss, 'provider_location'))
self.test_case.assertEqual('new_snapshot_instance_id', ss.id)
self.test_case.assertEqual('new_snapshot_id', ss.snapshot_id)
def check_downgrade(self, engine):
ss_table = utils.load_table(self.table_name, engine)
db_result = engine.execute(ss_table.select())
self.test_case.assertTrue(db_result.rowcount > 0)
for ss in db_result:
self.test_case.assertFalse(hasattr(ss, 'provider_location'))
self.test_case.assertEqual('new_snapshot_instance_id', ss.id)
self.test_case.assertEqual('new_snapshot_id', ss.snapshot_id)

View File

@ -129,7 +129,8 @@ def create_snapshot(**kwargs):
'share_id': share['id'] if with_share else None,
'user_id': 'fake',
'project_id': 'fake',
'status': 'creating'
'status': 'creating',
'provider_location': 'fake',
}
return _create_db_row(db.share_snapshot_create, snapshot, kwargs)

View File

@ -52,6 +52,48 @@ def fake_snapshot(**kwargs):
return db_fakes.FakeModel(snapshot)
def expected_snapshot(id='fake_snapshot_id', **kwargs):
self_link = 'http://localhost/v1/fake/snapshots/%s' % id
bookmark_link = 'http://localhost/fake/snapshots/%s' % id
snapshot = {
'id': id,
'share_id': 'fakeshareid',
'created_at': datetime.datetime(1, 1, 1, 1, 1, 1),
'status': 'fakesnapstatus',
'name': 'displaysnapname',
'description': 'displaysnapdesc',
'share_size': 1,
'size': 1,
'share_proto': 'fakesnapproto',
'links': [
{
'href': self_link,
'rel': 'self',
},
{
'href': bookmark_link,
'rel': 'bookmark',
},
],
}
snapshot.update(kwargs)
return {'snapshot': snapshot}
def search_opts(**kwargs):
search_opts = {
'name': 'fake_name',
'status': 'fake_status',
'share_id': 'fake_share_id',
'sort_key': 'fake_sort_key',
'sort_dir': 'fake_sort_dir',
'offset': '1',
'limit': '1',
}
search_opts.update(kwargs)
return search_opts
def fake_access(**kwargs):
access = {
'id': 'fakeaccid',

View File

@ -60,6 +60,8 @@
"share_snapshot:force_delete": "rule:admin_api",
"share_snapshot:reset_status": "rule:admin_api",
"share_snapshot:manage_snapshot": "rule:admin_api",
"share_snapshot:unmanage_snapshot": "rule:admin_api",
"share_network:create": "",
"share_network:index": "",

View File

@ -2013,6 +2013,46 @@ class GenericShareDriverTestCase(test.TestCase):
self.assertEqual(FAKE_COLLATED_INFO, result)
def test_manage_snapshot_not_found(self):
snapshot_instance = {'id': 'snap_instance_id',
'provider_location': 'vol_snap_id'}
driver_options = {}
self.mock_object(
self._driver.volume_api, 'get_snapshot',
mock.Mock(side_effect=exception.VolumeSnapshotNotFound(
snapshot_id='vol_snap_id')))
self.assertRaises(exception.ManageInvalidShareSnapshot,
self._driver.manage_existing_snapshot,
snapshot_instance,
driver_options)
self._driver.volume_api.get_snapshot.assert_called_once_with(
self._context, 'vol_snap_id')
def test_manage_snapshot_valid(self):
snapshot_instance = {'id': 'snap_instance_id',
'provider_location': 'vol_snap_id'}
volume_snapshot = {'id': 'vol_snap_id', 'size': 1}
self.mock_object(self._driver.volume_api, 'get_snapshot',
mock.Mock(return_value=volume_snapshot))
ret_manage = self._driver.manage_existing_snapshot(
snapshot_instance, {})
self.assertEqual({'provider_location': 'vol_snap_id',
'size': 1}, ret_manage)
self._driver.volume_api.get_snapshot.assert_called_once_with(
self._context, 'vol_snap_id')
def test_unmanage_snapshot(self):
snapshot_instance = {'id': 'snap_instance_id',
'provider_location': 'vol_snap_id'}
self.mock_object(self._driver.private_storage, 'delete')
self._driver.unmanage_snapshot(snapshot_instance)
self._driver.private_storage.delete.assert_called_once_with(
'snap_instance_id')
@generic.ensure_server
def fake(driver_instance, context, share_server=None):

View File

@ -218,15 +218,23 @@ class ShareDriverTestCase(test.TestCase):
share_driver.teardown_server,
'fake_share_server_details')
def _assert_is_callable(self, obj, attr):
self.assertTrue(callable(getattr(obj, attr)))
@ddt.data('manage_existing',
'unmanage')
def test_drivers_methods_needed_by_manage_functionality(self, method):
share_driver = self._instantiate_share_driver(None, False)
def assert_is_callable(obj, attr):
self.assertTrue(callable(getattr(obj, attr)))
self._assert_is_callable(share_driver, method)
assert_is_callable(share_driver, method)
@ddt.data('manage_existing_snapshot',
'unmanage_snapshot')
def test_drivers_methods_needed_by_manage_snapshot_functionality(
self, method):
share_driver = self._instantiate_share_driver(None, False)
self._assert_is_callable(share_driver, method)
@ddt.data(True, False)
def test_get_share_server_pools(self, value):

View File

@ -1240,7 +1240,7 @@ class ShareManagerTestCase(test.TestCase):
def _fake_create_snapshot(self, snapshot, **kwargs):
snapshot['progress'] = '99%'
return snapshot
return snapshot.to_dict()
self.mock_object(self.share_manager.driver, "create_snapshot",
_fake_create_snapshot)
@ -1249,7 +1249,6 @@ class ShareManagerTestCase(test.TestCase):
share_id = share['id']
snapshot = db_utils.create_snapshot(share_id=share_id)
snapshot_id = snapshot['id']
self.share_manager.create_snapshot(self.context, share_id,
snapshot_id)
self.assertEqual(share_id,
@ -3614,6 +3613,220 @@ class ShareManagerTestCase(test.TestCase):
manager._migrate_share_generic,
self.context, share, host)
def test_manage_snapshot_invalid_driver_mode(self):
self.mock_object(self.share_manager, 'driver')
self.share_manager.driver.driver_handles_share_servers = True
share = db_utils.create_share()
snapshot = db_utils.create_snapshot(share_id=share['id'])
driver_options = {'fake': 'fake'}
self.assertRaises(
exception.InvalidDriverMode,
self.share_manager.manage_snapshot, self.context,
snapshot['id'], driver_options)
def test_manage_snapshot_invalid_snapshot(self):
fake_share_server = 'fake_share_server'
self.mock_object(self.share_manager, 'driver')
self.share_manager.driver.driver_handles_share_servers = False
mock_get_share_server = self.mock_object(
self.share_manager,
'_get_share_server',
mock.Mock(return_value=fake_share_server))
share = db_utils.create_share()
snapshot = db_utils.create_snapshot(share_id=share['id'])
driver_options = {'fake': 'fake'}
mock_get = self.mock_object(self.share_manager.db,
'share_snapshot_get',
mock.Mock(return_value=snapshot))
self.assertRaises(
exception.InvalidShareSnapshot,
self.share_manager.manage_snapshot, self.context,
snapshot['id'], driver_options)
mock_get.assert_called_once_with(
utils.IsAMatcher(context.RequestContext), snapshot['id'])
mock_get_share_server.assert_called_once_with(
utils.IsAMatcher(context.RequestContext), snapshot['share'])
def test_manage_snapshot_driver_exception(self):
CustomException = type('CustomException', (Exception,), {})
self.mock_object(self.share_manager, 'driver')
self.share_manager.driver.driver_handles_share_servers = False
mock_manage = self.mock_object(self.share_manager.driver,
'manage_existing_snapshot',
mock.Mock(side_effect=CustomException))
mock_get_share_server = self.mock_object(self.share_manager,
'_get_share_server',
mock.Mock(return_value=None))
share = db_utils.create_share()
snapshot = db_utils.create_snapshot(share_id=share['id'])
driver_options = {}
mock_get = self.mock_object(self.share_manager.db,
'share_snapshot_get',
mock.Mock(return_value=snapshot))
self.assertRaises(
CustomException,
self.share_manager.manage_snapshot,
self.context, snapshot['id'], driver_options)
mock_manage.assert_called_once_with(mock.ANY, driver_options)
mock_get.assert_called_once_with(
utils.IsAMatcher(context.RequestContext), snapshot['id'])
mock_get_share_server.assert_called_once_with(
utils.IsAMatcher(context.RequestContext), snapshot['share'])
@ddt.data(
{'size': 1},
{'size': 2, 'name': 'fake'},
{'size': 3})
def test_manage_snapshot_valid_snapshot(self, driver_data):
mock_get_share_server = self.mock_object(self.share_manager,
'_get_share_server',
mock.Mock(return_value=None))
self.mock_object(self.share_manager.db, 'share_snapshot_update')
self.mock_object(self.share_manager, 'driver')
self.mock_object(self.share_manager, '_update_quota_usages')
self.share_manager.driver.driver_handles_share_servers = False
mock_manage = self.mock_object(
self.share_manager.driver,
"manage_existing_snapshot",
mock.Mock(return_value=driver_data))
size = driver_data['size']
share = db_utils.create_share(size=size)
snapshot = db_utils.create_snapshot(share_id=share['id'], size=size)
snapshot_id = snapshot['id']
driver_options = {}
mock_get = self.mock_object(self.share_manager.db,
'share_snapshot_get',
mock.Mock(return_value=snapshot))
self.share_manager.manage_snapshot(self.context, snapshot_id,
driver_options)
mock_manage.assert_called_once_with(mock.ANY, driver_options)
valid_snapshot_data = {
'status': constants.STATUS_AVAILABLE}
valid_snapshot_data.update(driver_data)
self.share_manager.db.share_snapshot_update.assert_called_once_with(
utils.IsAMatcher(context.RequestContext),
snapshot_id, valid_snapshot_data)
self.share_manager._update_quota_usages.assert_called_once_with(
utils.IsAMatcher(context.RequestContext),
snapshot['project_id'],
{'snapshots': 1, 'snapshot_gigabytes': size})
mock_get_share_server.assert_called_once_with(
utils.IsAMatcher(context.RequestContext), snapshot['share'])
mock_get.assert_called_once_with(
utils.IsAMatcher(context.RequestContext), snapshot_id)
def test_unmanage_snapshot_invalid_driver_mode(self):
self.mock_object(self.share_manager, 'driver')
self.share_manager.driver.driver_handles_share_servers = True
share = db_utils.create_share()
snapshot = db_utils.create_snapshot(share_id=share['id'])
self.mock_object(self.share_manager.db, 'share_snapshot_update')
ret = self.share_manager.unmanage_snapshot(self.context,
snapshot['id'])
self.assertIsNone(ret)
self.share_manager.db.share_snapshot_update.assert_called_once_with(
utils.IsAMatcher(context.RequestContext),
snapshot['id'],
{'status': constants.STATUS_UNMANAGE_ERROR})
def test_unmanage_snapshot_invalid_snapshot(self):
self.mock_object(self.share_manager, 'driver')
self.share_manager.driver.driver_handles_share_servers = False
mock_get_share_server = self.mock_object(
self.share_manager,
'_get_share_server',
mock.Mock(return_value='fake_share_server'))
self.mock_object(self.share_manager.db, 'share_snapshot_update')
share = db_utils.create_share()
snapshot = db_utils.create_snapshot(share_id=share['id'])
mock_get = self.mock_object(self.share_manager.db,
'share_snapshot_get',
mock.Mock(return_value=snapshot))
ret = self.share_manager.unmanage_snapshot(self.context,
snapshot['id'])
self.assertIsNone(ret)
self.share_manager.db.share_snapshot_update.assert_called_once_with(
utils.IsAMatcher(context.RequestContext),
snapshot['id'],
{'status': constants.STATUS_UNMANAGE_ERROR})
mock_get.assert_called_once_with(
utils.IsAMatcher(context.RequestContext), snapshot['id'])
mock_get_share_server.assert_called_once_with(
utils.IsAMatcher(context.RequestContext), snapshot['share'])
def test_unmanage_snapshot_invalid_share(self):
self.mock_object(self.share_manager, 'driver')
self.share_manager.driver.driver_handles_share_servers = False
mock_unmanage = mock.Mock(
side_effect=exception.UnmanageInvalidShareSnapshot(reason="fake"))
self.mock_object(self.share_manager.driver, "unmanage_snapshot",
mock_unmanage)
mock_get_share_server = self.mock_object(
self.share_manager,
'_get_share_server',
mock.Mock(return_value=None))
self.mock_object(self.share_manager.db, 'share_snapshot_update')
share = db_utils.create_share()
snapshot = db_utils.create_snapshot(share_id=share['id'])
mock_get = self.mock_object(self.share_manager.db,
'share_snapshot_get',
mock.Mock(return_value=snapshot))
self.share_manager.unmanage_snapshot(self.context, snapshot['id'])
self.share_manager.db.share_snapshot_update.assert_called_once_with(
utils.IsAMatcher(context.RequestContext), snapshot['id'],
{'status': constants.STATUS_UNMANAGE_ERROR})
self.share_manager.driver.unmanage_snapshot.assert_called_once_with(
mock.ANY)
mock_get.assert_called_once_with(
utils.IsAMatcher(context.RequestContext), snapshot['id'])
mock_get_share_server.assert_called_once_with(
utils.IsAMatcher(context.RequestContext), snapshot['share'])
@ddt.data(False, True)
def test_unmanage_snapshot_valid_snapshot(self, quota_error):
if quota_error:
self.mock_object(quota.QUOTAS, 'reserve', mock.Mock(
side_effect=exception.ManilaException(message='error')))
mock_log_warning = self.mock_object(manager.LOG, 'warning')
self.mock_object(self.share_manager, 'driver')
self.share_manager.driver.driver_handles_share_servers = False
self.mock_object(self.share_manager.driver, "unmanage_snapshot")
mock_get_share_server = self.mock_object(
self.share_manager,
'_get_share_server',
mock.Mock(return_value=None))
self.mock_object(self.share_manager.db, 'share_snapshot_destroy')
share = db_utils.create_share()
snapshot = db_utils.create_snapshot(share_id=share['id'])
mock_get = self.mock_object(self.share_manager.db,
'share_snapshot_get',
mock.Mock(return_value=snapshot))
self.share_manager.unmanage_snapshot(self.context, snapshot['id'])
self.share_manager.driver.unmanage_snapshot.assert_called_once_with(
mock.ANY)
self.share_manager.db.share_snapshot_destroy.assert_called_once_with(
mock.ANY, snapshot['id'])
mock_get.assert_called_once_with(
utils.IsAMatcher(context.RequestContext), snapshot['id'])
mock_get_share_server.assert_called_once_with(
utils.IsAMatcher(context.RequestContext), snapshot['share'])
if quota_error:
self.assertTrue(mock_log_warning.called)
@ddt.ddt
class HookWrapperTestCase(test.TestCase):

View File

@ -280,6 +280,21 @@ class ShareRpcAPITestCase(test.TestCase):
version='1.8',
share_replica=self.fake_share_replica)
def test_manage_snapshot(self):
self._test_share_api('manage_snapshot',
rpc_method='cast',
version='1.9',
snapshot=self.fake_snapshot,
host='fake_host',
driver_options={'volume_snapshot_id': 'fake'})
def test_unmanage_snapshot(self):
self._test_share_api('unmanage_snapshot',
rpc_method='cast',
version='1.9',
snapshot=self.fake_snapshot,
host='fake_host')
class Desthost(object):
host = 'fake_host'
capabilities = 1

View File

@ -227,6 +227,20 @@ class ManilaExceptionResponseCode400(test.TestCase):
self.assertEqual(400, e.code)
self.assertIn(reason, e.msg)
def test_manage_invalid_share_snapshot(self):
# Verify response code for exception.ManageInvalidShareSnapshot
reason = "fake_reason"
e = exception.ManageInvalidShareSnapshot(reason=reason)
self.assertEqual(400, e.code)
self.assertIn(reason, e.msg)
def test_unmanage_invalid_share_snapshot(self):
# Verify response code for exception.UnmanageInvalidShareSnapshot
reason = "fake_reason"
e = exception.UnmanageInvalidShareSnapshot(reason=reason)
self.assertEqual(400, e.code)
self.assertIn(reason, e.msg)
class ManilaExceptionResponseCode403(test.TestCase):
@ -490,6 +504,13 @@ class ManilaExceptionResponseCode404(test.TestCase):
self.assertEqual(404, e.code)
self.assertIn(share_id, e.msg)
def test_share_not_found(self):
# verify response code for exception.ShareNotFound
share_id = "fake_share_id"
e = exception.ShareNotFound(share_id=share_id)
self.assertEqual(404, e.code)
self.assertIn(share_id, e.msg)
class ManilaExceptionResponseCode413(test.TestCase):

View File

@ -36,7 +36,7 @@ ShareGroup = [
help="The minimum api microversion is configured to be the "
"value of the minimum microversion supported by Manila."),
cfg.StrOpt("max_api_microversion",
default="2.11",
default="2.12",
help="The maximum api microversion is configured to be the "
"value of the latest microversion supported by Manila."),
cfg.StrOpt("region",
@ -128,11 +128,6 @@ ShareGroup = [
help="Whether to suppress errors with clean up operation "
"or not. There are cases when we may want to skip "
"such errors and catch only test errors."),
cfg.BoolOpt("run_manage_unmanage_tests",
default=False,
help="Defines whether to run manage/unmanage tests or not. "
"These test may leave orphaned resources, so be careful "
"enabling this opt."),
# Switching ON/OFF test suites filtered by features
cfg.BoolOpt("run_quota_tests",
@ -161,6 +156,16 @@ ShareGroup = [
cfg.BoolOpt("run_migration_tests",
default=False,
help="Enable or disable migration tests."),
cfg.BoolOpt("run_manage_unmanage_tests",
default=False,
help="Defines whether to run manage/unmanage tests or not. "
"These test may leave orphaned resources, so be careful "
"enabling this opt."),
cfg.BoolOpt("run_manage_unmanage_snapshot_tests",
default=False,
help="Defines whether to run manage/unmanage snapshot tests "
"or not. These tests may leave orphaned resources, so be "
"careful enabling this opt."),
cfg.StrOpt("image_with_share_tools",
default="manila-service-image",

View File

@ -437,6 +437,113 @@ class SharesV2Client(shares_client.SharesClient):
self.expected_success(202, resp.status)
return body
###############
def create_snapshot(self, share_id, name=None, description=None,
force=False, version=LATEST_MICROVERSION):
if name is None:
name = data_utils.rand_name("tempest-created-share-snap")
if description is None:
description = data_utils.rand_name(
"tempest-created-share-snap-desc")
post_body = {
"snapshot": {
"name": name,
"force": force,
"description": description,
"share_id": share_id,
}
}
body = json.dumps(post_body)
resp, body = self.post("snapshots", body, version=version)
self.expected_success(202, resp.status)
return self._parse_resp(body)
def get_snapshot(self, snapshot_id, version=LATEST_MICROVERSION):
resp, body = self.get("snapshots/%s" % snapshot_id, version=version)
self.expected_success(200, resp.status)
return self._parse_resp(body)
def list_snapshots(self, detailed=False, params=None,
version=LATEST_MICROVERSION):
"""Get list of share snapshots w/o filters."""
uri = 'snapshots/detail' if detailed else 'snapshots'
uri += '?%s' % urlparse.urlencode(params) if params else ''
resp, body = self.get(uri, version=version)
self.expected_success(200, resp.status)
return self._parse_resp(body)
def list_snapshots_with_detail(self, params=None,
version=LATEST_MICROVERSION):
"""Get detailed list of share snapshots w/o filters."""
return self.list_snapshots(detailed=True, params=params,
version=version)
def delete_snapshot(self, snap_id, version=LATEST_MICROVERSION):
resp, body = self.delete("snapshots/%s" % snap_id, version=version)
self.expected_success(202, resp.status)
return body
def wait_for_snapshot_status(self, snapshot_id, status,
version=LATEST_MICROVERSION):
"""Waits for a snapshot to reach a given status."""
body = self.get_snapshot(snapshot_id, version=version)
snapshot_name = body['name']
snapshot_status = body['status']
start = int(time.time())
while snapshot_status != status:
time.sleep(self.build_interval)
body = self.get_snapshot(snapshot_id, version=version)
snapshot_status = body['status']
if 'error' in snapshot_status:
raise (share_exceptions.
SnapshotBuildErrorException(snapshot_id=snapshot_id))
if int(time.time()) - start >= self.build_timeout:
message = ('Share Snapshot %s failed to reach %s status '
'within the required time (%s s).' %
(snapshot_name, status, self.build_timeout))
raise exceptions.TimeoutException(message)
def manage_snapshot(self, share_id, provider_location,
name=None, description=None,
version=LATEST_MICROVERSION,
driver_options=None):
if name is None:
name = data_utils.rand_name("tempest-manage-snapshot")
if description is None:
description = data_utils.rand_name("tempest-manage-snapshot-desc")
post_body = {
"snapshot": {
"share_id": share_id,
"provider_location": provider_location,
"name": name,
"description": description,
"driver_options": driver_options if driver_options else {},
}
}
url = 'snapshots/manage'
body = json.dumps(post_body)
resp, body = self.post(url, body, version=version)
self.expected_success(202, resp.status)
return self._parse_resp(body)
def unmanage_snapshot(self, snapshot_id, version=LATEST_MICROVERSION,
body=None):
url = 'snapshots'
action_name = 'action'
if body is None:
body = json.dumps({'unmanage': {}})
resp, body = self.post(
"%(url)s/%(snapshot_id)s/%(action_name)s" % {
'url': url, 'snapshot_id': snapshot_id,
'action_name': action_name},
body,
version=version)
self.expected_success(202, resp.status)
return body
###############
def _get_access_action_name(self, version, action):

View File

@ -0,0 +1,143 @@
# Copyright 2015 EMC Corporation.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import six
from tempest import config
from tempest import test
from tempest_lib.common.utils import data_utils
from tempest_lib import exceptions as lib_exc
import testtools
from manila_tempest_tests.tests.api import base
CONF = config.CONF
class ManageNFSSnapshotTest(base.BaseSharesAdminTest):
protocol = 'nfs'
# NOTE(vponomaryov): be careful running these tests using generic driver
# because cinder volume snapshots won't be deleted.
@classmethod
@base.skip_if_microversion_lt("2.12")
@testtools.skipIf(
CONF.share.multitenancy_enabled,
"Only for driver_handles_share_servers = False driver mode.")
@testtools.skipUnless(
CONF.share.run_manage_unmanage_snapshot_tests,
"Manage/unmanage snapshot tests are disabled.")
def resource_setup(cls):
super(ManageNFSSnapshotTest, cls).resource_setup()
if cls.protocol not in CONF.share.enable_protocols:
message = "%s tests are disabled" % cls.protocol
raise cls.skipException(message)
# Create share type
cls.st_name = data_utils.rand_name("tempest-manage-st-name")
cls.extra_specs = {
'storage_protocol': CONF.share.capability_storage_protocol,
'driver_handles_share_servers': False,
'snapshot_support': six.text_type(
CONF.share.capability_snapshot_support),
}
cls.st = cls.create_share_type(
name=cls.st_name,
cleanup_in_class=True,
extra_specs=cls.extra_specs)
creation_data = {'kwargs': {
'share_type_id': cls.st['share_type']['id'],
'share_protocol': cls.protocol,
}}
# Data for creating shares
data = [creation_data]
shares_created = cls.create_shares(data)
cls.snapshot = None
cls.shares = []
# Load all share data (host, etc.)
for share in shares_created:
cls.shares.append(cls.shares_v2_client.get_share(share['id']))
# Create snapshot
snap_name = data_utils.rand_name("tempest-snapshot-name")
snap_desc = data_utils.rand_name(
"tempest-snapshot-description")
snap = cls.create_snapshot_wait_for_active(
share['id'], snap_name, snap_desc)
cls.snapshot = cls.shares_v2_client.get_snapshot(snap['id'])
# Unmanage snapshot
cls.shares_v2_client.unmanage_snapshot(snap['id'])
cls.shares_client.wait_for_resource_deletion(
snapshot_id=snap['id'])
def _test_manage(self, snapshot, version=CONF.share.max_api_microversion):
name = ("Name for 'managed' snapshot that had ID %s" %
snapshot['id'])
description = "Description for 'managed' snapshot"
# Manage snapshot
share_id = snapshot['share_id']
snapshot = self.shares_v2_client.manage_snapshot(
share_id,
snapshot['provider_location'],
name=name,
description=description,
driver_options={}
)
# Add managed snapshot to cleanup queue
self.method_resources.insert(
0, {'type': 'snapshot', 'id': snapshot['id'],
'client': self.shares_v2_client})
# Wait for success
self.shares_v2_client.wait_for_snapshot_status(snapshot['id'],
'available')
# Verify data of managed snapshot
get_snapshot = self.shares_v2_client.get_snapshot(snapshot['id'])
self.assertEqual(name, get_snapshot['name'])
self.assertEqual(description, get_snapshot['description'])
self.assertEqual(snapshot['share_id'], get_snapshot['share_id'])
self.assertEqual(snapshot['provider_location'],
get_snapshot['provider_location'])
# Delete snapshot
self.shares_v2_client.delete_snapshot(get_snapshot['id'])
self.shares_client.wait_for_resource_deletion(
snapshot_id=get_snapshot['id'])
self.assertRaises(lib_exc.NotFound,
self.shares_v2_client.get_snapshot,
get_snapshot['id'])
@test.attr(type=["gate", "smoke"])
def test_manage(self):
# Manage snapshot
self._test_manage(snapshot=self.snapshot)
class ManageCIFSSnapshotTest(ManageNFSSnapshotTest):
protocol = 'cifs'
class ManageGLUSTERFSSnapshotTest(ManageNFSSnapshotTest):
protocol = 'glusterfs'
class ManageHDFSSnapshotTest(ManageNFSSnapshotTest):
protocol = 'hdfs'

View File

@ -0,0 +1,109 @@
# Copyright 2015 EMC Corporation.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import six
from tempest import config
from tempest import test
from tempest_lib.common.utils import data_utils
from tempest_lib import exceptions as lib_exc
import testtools
from manila_tempest_tests.tests.api import base
CONF = config.CONF
class ManageNFSSnapshotNegativeTest(base.BaseSharesAdminTest):
protocol = 'nfs'
@classmethod
@base.skip_if_microversion_lt("2.12")
@testtools.skipIf(
CONF.share.multitenancy_enabled,
"Only for driver_handles_share_servers = False driver mode.")
@testtools.skipUnless(
CONF.share.run_manage_unmanage_snapshot_tests,
"Manage/unmanage snapshot tests are disabled.")
def resource_setup(cls):
super(ManageNFSSnapshotNegativeTest, cls).resource_setup()
if cls.protocol not in CONF.share.enable_protocols:
message = "%s tests are disabled" % cls.protocol
raise cls.skipException(message)
# Create share type
cls.st_name = data_utils.rand_name("tempest-manage-st-name")
cls.extra_specs = {
'storage_protocol': CONF.share.capability_storage_protocol,
'driver_handles_share_servers': False,
'snapshot_support': six.text_type(
CONF.share.capability_snapshot_support),
}
cls.st = cls.create_share_type(
name=cls.st_name,
cleanup_in_class=True,
extra_specs=cls.extra_specs)
# Create share
cls.share = cls.create_share(
share_type_id=cls.st['share_type']['id'],
share_protocol=cls.protocol
)
@test.attr(type=["gate", "smoke", "negative", ])
def test_manage_not_found(self):
# Manage snapshot fails
self.assertRaises(lib_exc.NotFound,
self.shares_v2_client.manage_snapshot,
'fake-share-id',
'fake-vol-snap-id',
driver_options={})
@test.attr(type=["gate", "smoke", "negative", ])
def test_manage_already_exists(self):
# Manage already existing snapshot fails
# Create snapshot
snap = self.create_snapshot_wait_for_active(self.share['id'])
get_snap = self.shares_v2_client.get_snapshot(snap['id'])
self.assertEqual(self.share['id'], get_snap['share_id'])
self.assertIsNotNone(get_snap['provider_location'])
# Manage snapshot fails
self.assertRaises(lib_exc.Conflict,
self.shares_v2_client.manage_snapshot,
self.share['id'],
get_snap['provider_location'],
driver_options={})
# Delete snapshot
self.shares_v2_client.delete_snapshot(get_snap['id'])
self.shares_client.wait_for_resource_deletion(
snapshot_id=get_snap['id'])
self.assertRaises(lib_exc.NotFound,
self.shares_v2_client.get_snapshot,
get_snap['id'])
class ManageCIFSSnapshotNegativeTest(ManageNFSSnapshotNegativeTest):
protocol = 'cifs'
class ManageGLUSTERFSSnapshotNegativeTest(ManageNFSSnapshotNegativeTest):
protocol = 'glusterfs'
class ManageHDFSSnapshotNegativeTest(ManageNFSSnapshotNegativeTest):
protocol = 'hdfs'

View File

@ -79,6 +79,7 @@ def network_synchronized(f):
skip_if_microversion_not_supported = utils.skip_if_microversion_not_supported
skip_if_microversion_lt = utils.skip_if_microversion_lt
class BaseSharesTest(test.BaseTestCase):
@ -104,6 +105,13 @@ class BaseSharesTest(test.BaseTestCase):
raise self.skipException(
"Microversion '%s' is not supported." % microversion)
def skip_if_microversion_lt(self, microversion):
if utils.is_microversion_lt(CONF.share.max_api_microversion,
microversion):
raise self.skipException(
"Microversion must be greater than or equal to '%s'." %
microversion)
@classmethod
def get_client_with_isolated_creds(cls,
name=None,

View File

@ -81,6 +81,15 @@ def skip_if_microversion_not_supported(microversion):
return lambda f: f
def skip_if_microversion_lt(microversion):
"""Decorator for tests that are microversion-specific."""
if is_microversion_lt(CONF.share.max_api_microversion, microversion):
reason = ("Skipped. Test requires microversion greater than or "
"equal to '%s'." % microversion)
return testtools.skip(reason)
return lambda f: f
def rand_ip():
"""This uses the TEST-NET-3 range of reserved IP addresses.

View File

@ -0,0 +1,3 @@
---
features:
- Manage and unmanage snapshot.