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:
parent
a9b6e2759e
commit
c91f27f4e1
@ -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
|
||||
|
@ -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::
|
||||
|
||||
|
@ -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",
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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())
|
||||
|
@ -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"]
|
||||
|
||||
|
177
manila/api/v2/share_snapshots.py
Normal file
177
manila/api/v2/share_snapshots.py
Normal 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())
|
@ -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']
|
||||
|
@ -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')
|
@ -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)
|
||||
|
||||
|
||||
|
@ -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=(
|
||||
|
@ -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.")
|
||||
|
||||
|
@ -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."""
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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(
|
||||
|
@ -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')
|
||||
|
@ -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 = (
|
||||
{
|
||||
|
@ -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')
|
||||
|
629
manila/tests/api/v2/test_share_snapshots.py
Normal file
629
manila/tests/api/v2/test_share_snapshots.py
Normal 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')
|
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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',
|
||||
|
@ -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": "",
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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):
|
||||
|
143
manila_tempest_tests/tests/api/admin/test_snapshot_manage.py
Normal file
143
manila_tempest_tests/tests/api/admin/test_snapshot_manage.py
Normal 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'
|
@ -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'
|
@ -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,
|
||||
|
@ -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.
|
||||
|
||||
|
@ -0,0 +1,3 @@
|
||||
---
|
||||
features:
|
||||
- Manage and unmanage snapshot.
|
Loading…
Reference in New Issue
Block a user