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_CG_TESTS=${RUN_MANILA_CG_TESTS:-True}
|
||||||
RUN_MANILA_MANAGE_TESTS=${RUN_MANILA_MANAGE_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}
|
MANILA_CONF=${MANILA_CONF:-/etc/manila/manila.conf}
|
||||||
|
|
||||||
@ -128,6 +129,7 @@ if [[ "$TEST_TYPE" == "scenario" ]]; then
|
|||||||
echo "Set test set to scenario only"
|
echo "Set test set to scenario only"
|
||||||
MANILA_TESTS='manila_tempest_tests.tests.scenario'
|
MANILA_TESTS='manila_tempest_tests.tests.scenario'
|
||||||
elif [[ "$DRIVER" == "generic" ]]; then
|
elif [[ "$DRIVER" == "generic" ]]; then
|
||||||
|
RUN_MANILA_MANAGE_SNAPSHOT_TESTS=True
|
||||||
if [[ "$POSTGRES_ENABLED" == "True" ]]; then
|
if [[ "$POSTGRES_ENABLED" == "True" ]]; then
|
||||||
# Run only CIFS tests on PostgreSQL DB backend
|
# Run only CIFS tests on PostgreSQL DB backend
|
||||||
# to reduce amount of tests per job using 'generic' share driver.
|
# 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
|
# Enable manage/unmanage tests
|
||||||
iniset $TEMPEST_CONFIG share run_manage_unmanage_tests $RUN_MANILA_MANAGE_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
|
# Also, we should wait until service VM is available
|
||||||
# before running Tempest tests using Generic driver in DHSS=False mode.
|
# before running Tempest tests using Generic driver in DHSS=False mode.
|
||||||
source $BASE/new/manila/contrib/ci/common.sh
|
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
|
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 |
|
| 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 |
|
| 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 |
|
| NetApp Clustered Data ONTAP | DHSS = True (J) & False (K) | L | L | L | J | J | \- |
|
||||||
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+
|
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
|
||||||
| EMC VNX | DHSS = True (J) | \- | \- | \- | J | J |
|
| EMC VNX | DHSS = True (J) | \- | \- | \- | J | J | \- |
|
||||||
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+
|
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
|
||||||
| EMC Isilon | DHSS = False (K) | \- | M | \- | K | K |
|
| EMC Isilon | DHSS = False (K) | \- | M | \- | K | K | \- |
|
||||||
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+
|
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
|
||||||
| Red Hat GlusterFS | DHSS = False (J) | \- | \- | \- | volume layout (L) | volume layout (L) |
|
| Red Hat GlusterFS | DHSS = False (J) | \- | \- | \- | volume layout (L) | volume layout (L) | \- |
|
||||||
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+
|
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
|
||||||
| Red Hat GlusterFS-Native | DHSS = False (J) | \- | \- | \- | K | L |
|
| Red Hat GlusterFS-Native | DHSS = False (J) | \- | \- | \- | K | L | \- |
|
||||||
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+
|
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
|
||||||
| HDFS | DHSS = False (K) | \- | M | \- | K | K |
|
| HDFS | DHSS = False (K) | \- | M | \- | K | K | \- |
|
||||||
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+
|
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
|
||||||
| Hitachi HNAS | DHSS = False (L) | L | L | M | L | L |
|
| Hitachi HNAS | DHSS = False (L) | L | L | M | L | L | \- |
|
||||||
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+
|
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
|
||||||
| HPE 3PAR | DHSS = True (L) & False (K) | \- | \- | \- | K | K |
|
| HPE 3PAR | DHSS = True (L) & False (K) | \- | \- | \- | K | K | \- |
|
||||||
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+
|
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
|
||||||
| Huawei | DHSS = True (M) & False(K) | L | L | L | K | M |
|
| Huawei | DHSS = True (M) & False(K) | L | L | L | K | M | \- |
|
||||||
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+
|
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
|
||||||
| IBM GPFS | DHSS = False(K) | \- | L | \- | K | K |
|
| IBM GPFS | DHSS = False(K) | \- | L | \- | K | K | \- |
|
||||||
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+
|
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
|
||||||
| LVM | DHSS = False (M) | \- | M | \- | M | M |
|
| LVM | DHSS = False (M) | \- | M | \- | M | M | \- |
|
||||||
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+
|
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
|
||||||
| Quobyte | DHSS = False (K) | \- | M | M | \- | \- |
|
| Quobyte | DHSS = False (K) | \- | M | M | \- | \- | \- |
|
||||||
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+
|
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
|
||||||
| Windows SMB | DHSS = True (L) & False (L) | L | L | L | L | L |
|
| Windows SMB | DHSS = True (L) & False (L) | L | L | L | L | L | \- |
|
||||||
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+
|
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
|
||||||
| Oracle ZFSSA | DHSS = False (K) | \- | \- | \- | K | K |
|
| Oracle ZFSSA | DHSS = False (K) | \- | \- | \- | K | K | \- |
|
||||||
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+
|
+----------------------------------------+-----------------------------+-----------------------+--------------+--------------+------------------------+----------------------------+--------------------------+
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
|
@ -52,6 +52,8 @@
|
|||||||
"share_snapshot:get_snapshot": "rule:default",
|
"share_snapshot:get_snapshot": "rule:default",
|
||||||
"share_snapshot:get_all_snapshots": "rule:default",
|
"share_snapshot:get_all_snapshots": "rule:default",
|
||||||
"share_snapshot:snapshot_update": "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:force_delete": "rule:admin_api",
|
||||||
"share_snapshot:reset_status": "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
|
* 2.10 - Field 'access_rules_status' was added to shares and share
|
||||||
instances.
|
instances.
|
||||||
* 2.11 - Share Replication support
|
* 2.11 - Share Replication support
|
||||||
|
* 2.12 - Manage/unmanage snapshot API.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# The minimum and maximum versions of the API supported
|
# The minimum and maximum versions of the API supported
|
||||||
# The default api version request is defined to be the
|
# The default api version request is defined to be the
|
||||||
# the minimum version of the API supported.
|
# the minimum version of the API supported.
|
||||||
_MIN_API_VERSION = "2.0"
|
_MIN_API_VERSION = "2.0"
|
||||||
_MAX_API_VERSION = "2.11"
|
_MAX_API_VERSION = "2.12"
|
||||||
DEFAULT_API_VERSION = _MIN_API_VERSION
|
DEFAULT_API_VERSION = _MIN_API_VERSION
|
||||||
|
|
||||||
|
|
||||||
|
@ -85,3 +85,7 @@ user documentation.
|
|||||||
'Experimental'. Share APIs return two new attributes: 'has_replicas' and
|
'Experimental'. Share APIs return two new attributes: 'has_replicas' and
|
||||||
'replication_type'. Share instance APIs return a new attribute,
|
'replication_type'. Share instance APIs return a new attribute,
|
||||||
'replica_state'.
|
'replica_state'.
|
||||||
|
|
||||||
|
2.12
|
||||||
|
----
|
||||||
|
Share snapshot manage and unmanage API.
|
||||||
|
@ -30,15 +30,8 @@ from manila import share
|
|||||||
LOG = log.getLogger(__name__)
|
LOG = log.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class ShareSnapshotsController(wsgi.Controller, wsgi.AdminActionsMixin):
|
class ShareSnapshotMixin(object):
|
||||||
"""The Share Snapshots API controller for the OpenStack API."""
|
"""Mixin class for Share Snapshot Controllers."""
|
||||||
|
|
||||||
resource_name = 'share_snapshot'
|
|
||||||
_view_builder_class = snapshot_views.ViewBuilder
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
super(ShareSnapshotsController, self).__init__()
|
|
||||||
self.share_api = share.API()
|
|
||||||
|
|
||||||
def _update(self, *args, **kwargs):
|
def _update(self, *args, **kwargs):
|
||||||
db.share_snapshot_update(*args, **kwargs)
|
db.share_snapshot_update(*args, **kwargs)
|
||||||
@ -49,26 +42,6 @@ class ShareSnapshotsController(wsgi.Controller, wsgi.AdminActionsMixin):
|
|||||||
def _delete(self, *args, **kwargs):
|
def _delete(self, *args, **kwargs):
|
||||||
return self.share_api.delete_snapshot(*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):
|
def show(self, req, id):
|
||||||
"""Return data about the given snapshot."""
|
"""Return data about the given snapshot."""
|
||||||
context = req.environ['manila.context']
|
context = req.environ['manila.context']
|
||||||
@ -219,5 +192,25 @@ class ShareSnapshotsController(wsgi.Controller, wsgi.AdminActionsMixin):
|
|||||||
req, dict(new_snapshot.items()))
|
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():
|
def create_resource():
|
||||||
return wsgi.Resource(ShareSnapshotsController())
|
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_metadata
|
||||||
from manila.api.v1 import share_networks
|
from manila.api.v1 import share_networks
|
||||||
from manila.api.v1 import share_servers
|
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_types_extra_specs
|
||||||
from manila.api.v1 import share_unmanage
|
from manila.api.v1 import share_unmanage
|
||||||
from manila.api.v2 import availability_zones
|
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_instance_export_locations
|
||||||
from manila.api.v2 import share_instances
|
from manila.api.v2 import share_instances
|
||||||
from manila.api.v2 import share_replicas
|
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 share_types
|
||||||
from manila.api.v2 import shares
|
from manila.api.v2 import shares
|
||||||
from manila.api import versions
|
from manila.api import versions
|
||||||
@ -199,6 +199,12 @@ class APIRouter(manila.api.openstack.APIRouter):
|
|||||||
collection={"detail": "GET"},
|
collection={"detail": "GET"},
|
||||||
member={"action": "POST"})
|
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()
|
self.resources["share_metadata"] = share_metadata.create_resource()
|
||||||
share_metadata_controller = self.resources["share_metadata"]
|
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."""
|
"""Model a server API response as a python dictionary."""
|
||||||
|
|
||||||
_collection_name = 'snapshots'
|
_collection_name = 'snapshots'
|
||||||
|
_detail_version_modifiers = [
|
||||||
|
"add_provider_location_field",
|
||||||
|
]
|
||||||
|
|
||||||
def summary_list(self, request, snapshots):
|
def summary_list(self, request, snapshots):
|
||||||
"""Show a list of share snapshots without many details."""
|
"""Show a list of share snapshots without many details."""
|
||||||
@ -41,21 +44,31 @@ class ViewBuilder(common.ViewBuilder):
|
|||||||
|
|
||||||
def detail(self, request, snapshot):
|
def detail(self, request, snapshot):
|
||||||
"""Detailed view of a single share snapshot."""
|
"""Detailed view of a single share snapshot."""
|
||||||
return {
|
snapshot_dict = {
|
||||||
'snapshot': {
|
'id': snapshot.get('id'),
|
||||||
'id': snapshot.get('id'),
|
'share_id': snapshot.get('share_id'),
|
||||||
'share_id': snapshot.get('share_id'),
|
'share_size': snapshot.get('share_size'),
|
||||||
'share_size': snapshot.get('share_size'),
|
'created_at': snapshot.get('created_at'),
|
||||||
'created_at': snapshot.get('created_at'),
|
'status': snapshot.get('status'),
|
||||||
'status': snapshot.get('status'),
|
'name': snapshot.get('display_name'),
|
||||||
'name': snapshot.get('display_name'),
|
'description': snapshot.get('display_description'),
|
||||||
'description': snapshot.get('display_description'),
|
'size': snapshot.get('size'),
|
||||||
'size': snapshot.get('size'),
|
'share_proto': snapshot.get('share_proto'),
|
||||||
'share_proto': snapshot.get('share_proto'),
|
'links': self._get_links(request, snapshot['id']),
|
||||||
'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):
|
def _list_view(self, func, request, snapshots):
|
||||||
"""Provide a view for a list of share snapshots."""
|
"""Provide a view for a list of share snapshots."""
|
||||||
snapshots_list = [func(request, snapshot)['snapshot']
|
snapshots_list = [func(request, snapshot)['snapshot']
|
||||||
|
@ -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):
|
def extract_snapshot_instance_values(values):
|
||||||
fields = ['status', 'progress']
|
fields = ['status', 'progress', 'provider_location']
|
||||||
return extract_instance_values(values, fields)
|
return extract_instance_values(values, fields)
|
||||||
|
|
||||||
|
|
||||||
|
@ -583,7 +583,8 @@ class ShareInstanceAccessMapping(BASE, ManilaBase):
|
|||||||
class ShareSnapshot(BASE, ManilaBase):
|
class ShareSnapshot(BASE, ManilaBase):
|
||||||
"""Represents a snapshot of a share."""
|
"""Represents a snapshot of a share."""
|
||||||
__tablename__ = 'share_snapshots'
|
__tablename__ = 'share_snapshots'
|
||||||
_extra_keys = ['name', 'share_name', 'status', 'progress']
|
_extra_keys = ['name', 'share_name', 'status', 'progress',
|
||||||
|
'provider_location']
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def name(self):
|
||||||
@ -603,6 +604,11 @@ class ShareSnapshot(BASE, ManilaBase):
|
|||||||
if self.instance:
|
if self.instance:
|
||||||
return self.instance.progress
|
return self.instance.progress
|
||||||
|
|
||||||
|
@property
|
||||||
|
def provider_location(self):
|
||||||
|
if self.instance:
|
||||||
|
return self.instance.provider_location
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def instance(self):
|
def instance(self):
|
||||||
if len(self.instances) > 0:
|
if len(self.instances) > 0:
|
||||||
@ -664,6 +670,7 @@ class ShareSnapshotInstance(BASE, ManilaBase):
|
|||||||
String(36), ForeignKey('share_instances.id'), nullable=False)
|
String(36), ForeignKey('share_instances.id'), nullable=False)
|
||||||
status = Column(String(255))
|
status = Column(String(255))
|
||||||
progress = Column(String(255))
|
progress = Column(String(255))
|
||||||
|
provider_location = Column(String(255))
|
||||||
share_instance = orm.relationship(
|
share_instance = orm.relationship(
|
||||||
ShareInstance, backref="snapshot_instances",
|
ShareInstance, backref="snapshot_instances",
|
||||||
primaryjoin=(
|
primaryjoin=(
|
||||||
|
@ -426,6 +426,10 @@ class ExportLocationNotFound(NotFound):
|
|||||||
message = _("Export location %(uuid)s could not be found.")
|
message = _("Export location %(uuid)s could not be found.")
|
||||||
|
|
||||||
|
|
||||||
|
class ShareNotFound(NotFound):
|
||||||
|
message = _("Share %(share_id)s could not be found.")
|
||||||
|
|
||||||
|
|
||||||
class ShareSnapshotNotFound(NotFound):
|
class ShareSnapshotNotFound(NotFound):
|
||||||
message = _("Snapshot %(snapshot_id)s could not be found.")
|
message = _("Snapshot %(snapshot_id)s could not be found.")
|
||||||
|
|
||||||
@ -443,6 +447,16 @@ class InvalidShareSnapshot(Invalid):
|
|||||||
message = _("Invalid share snapshot: %(reason)s.")
|
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):
|
class ShareMetadataNotFound(NotFound):
|
||||||
message = _("Metadata item is not found.")
|
message = _("Metadata item is not found.")
|
||||||
|
|
||||||
|
@ -517,6 +517,53 @@ class API(base.Base):
|
|||||||
# share server here, when manage/unmanage operations will be supported
|
# share server here, when manage/unmanage operations will be supported
|
||||||
# for driver_handles_share_servers=True mode
|
# 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')
|
@policy.wrap_check_policy('share')
|
||||||
def delete(self, context, share, force=False):
|
def delete(self, context, share, force=False):
|
||||||
"""Delete share."""
|
"""Delete share."""
|
||||||
|
@ -826,6 +826,40 @@ class ShareDriver(object):
|
|||||||
UnmanageInvalidShare exception, specifying a reason for the failure.
|
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):
|
def extend_share(self, share, new_size, share_server=None):
|
||||||
"""Extends size of existing share.
|
"""Extends size of existing share.
|
||||||
|
|
||||||
|
@ -745,6 +745,7 @@ class GenericShareDriver(driver.ExecuteMixin, driver.ShareDriver):
|
|||||||
|
|
||||||
def create_snapshot(self, context, snapshot, share_server=None):
|
def create_snapshot(self, context, snapshot, share_server=None):
|
||||||
"""Creates a snapshot."""
|
"""Creates a snapshot."""
|
||||||
|
model_update = {}
|
||||||
volume = self._get_volume(self.admin_context, snapshot['share_id'])
|
volume = self._get_volume(self.admin_context, snapshot['share_id'])
|
||||||
volume_snapshot_name = (self.configuration.
|
volume_snapshot_name = (self.configuration.
|
||||||
volume_snapshot_name_template % snapshot['id'])
|
volume_snapshot_name_template % snapshot['id'])
|
||||||
@ -762,14 +763,22 @@ class GenericShareDriver(driver.ExecuteMixin, driver.ShareDriver):
|
|||||||
self.admin_context,
|
self.admin_context,
|
||||||
volume_snapshot['id'])
|
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(
|
self.private_storage.update(
|
||||||
snapshot['id'], {'volume_snapshot_id': volume_snapshot['id']})
|
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:
|
else:
|
||||||
raise exception.ManilaException(
|
raise exception.ManilaException(
|
||||||
_('Volume snapshot have not been '
|
_('Volume snapshot have not been '
|
||||||
'created in %ss. Giving up') %
|
'created in %ss. Giving up') %
|
||||||
self.configuration.max_time_to_create_volume)
|
self.configuration.max_time_to_create_volume)
|
||||||
|
|
||||||
|
return model_update
|
||||||
|
|
||||||
def delete_snapshot(self, context, snapshot, share_server=None):
|
def delete_snapshot(self, context, snapshot, share_server=None):
|
||||||
"""Deletes a snapshot."""
|
"""Deletes a snapshot."""
|
||||||
volume_snapshot = self._get_volume_snapshot(self.admin_context,
|
volume_snapshot = self._get_volume_snapshot(self.admin_context,
|
||||||
@ -935,6 +944,46 @@ class GenericShareDriver(driver.ExecuteMixin, driver.ShareDriver):
|
|||||||
server_details, old_export_location)
|
server_details, old_export_location)
|
||||||
return {'size': share_size, 'export_locations': export_locations}
|
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,
|
def _get_mount_stats_by_index(self, mount_path, server_details, index,
|
||||||
block_size='G'):
|
block_size='G'):
|
||||||
"""Get mount stats using df shell command.
|
"""Get mount stats using df shell command.
|
||||||
|
@ -164,7 +164,7 @@ def add_hooks(f):
|
|||||||
class ShareManager(manager.SchedulerDependentManager):
|
class ShareManager(manager.SchedulerDependentManager):
|
||||||
"""Manages NAS storages."""
|
"""Manages NAS storages."""
|
||||||
|
|
||||||
RPC_API_VERSION = '1.8'
|
RPC_API_VERSION = '1.9'
|
||||||
|
|
||||||
def __init__(self, share_driver=None, service_name=None, *args, **kwargs):
|
def __init__(self, share_driver=None, service_name=None, *args, **kwargs):
|
||||||
"""Load the driver from args, or from flags."""
|
"""Load the driver from args, or from flags."""
|
||||||
@ -1308,6 +1308,80 @@ class ShareManager(manager.SchedulerDependentManager):
|
|||||||
{'status': constants.STATUS_MANAGE_ERROR, 'size': 1})
|
{'status': constants.STATUS_MANAGE_ERROR, 'size': 1})
|
||||||
raise
|
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):
|
def _update_quota_usages(self, context, project_id, usages):
|
||||||
user_id = context.user_id
|
user_id = context.user_id
|
||||||
for resource, usage in usages.items():
|
for resource, usage in usages.items():
|
||||||
@ -1383,6 +1457,60 @@ class ShareManager(manager.SchedulerDependentManager):
|
|||||||
self.db.share_instance_delete(context, share_instance['id'])
|
self.db.share_instance_delete(context, share_instance['id'])
|
||||||
LOG.info(_LI("Share %s: unmanaged successfully."), share_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
|
@add_hooks
|
||||||
@utils.require_driver_initialized
|
@utils.require_driver_initialized
|
||||||
def delete_share_instance(self, context, share_instance_id):
|
def delete_share_instance(self, context, share_instance_id):
|
||||||
@ -1451,10 +1579,8 @@ class ShareManager(manager.SchedulerDependentManager):
|
|||||||
context, snapshot_instance, share_server=share_server)
|
context, snapshot_instance, share_server=share_server)
|
||||||
|
|
||||||
if model_update:
|
if model_update:
|
||||||
model_dict = model_update.to_dict()
|
|
||||||
self.db.share_snapshot_instance_update(
|
self.db.share_snapshot_instance_update(
|
||||||
context, snapshot_instance_id, model_dict)
|
context, snapshot_instance_id, model_update)
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
with excutils.save_and_reraise_exception():
|
with excutils.save_and_reraise_exception():
|
||||||
self.db.share_snapshot_instance_update(
|
self.db.share_snapshot_instance_update(
|
||||||
|
@ -51,6 +51,7 @@ class ShareAPI(object):
|
|||||||
delete_share_replica()
|
delete_share_replica()
|
||||||
promote_share_replica()
|
promote_share_replica()
|
||||||
update_share_replica()
|
update_share_replica()
|
||||||
|
1.9 - Add manage_snapshot() and unmanage_snapshot() methods
|
||||||
"""
|
"""
|
||||||
|
|
||||||
BASE_RPC_API_VERSION = '1.0'
|
BASE_RPC_API_VERSION = '1.0'
|
||||||
@ -59,7 +60,7 @@ class ShareAPI(object):
|
|||||||
super(ShareAPI, self).__init__()
|
super(ShareAPI, self).__init__()
|
||||||
target = messaging.Target(topic=CONF.share_topic,
|
target = messaging.Target(topic=CONF.share_topic,
|
||||||
version=self.BASE_RPC_API_VERSION)
|
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,
|
def create_share_instance(self, context, share_instance, host,
|
||||||
request_spec, filter_properties,
|
request_spec, filter_properties,
|
||||||
@ -87,6 +88,22 @@ class ShareAPI(object):
|
|||||||
call_context = self.client.prepare(server=host, version='1.1')
|
call_context = self.client.prepare(server=host, version='1.1')
|
||||||
call_context.cast(context, 'unmanage_share', share_id=share['id'])
|
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):
|
def delete_share_instance(self, context, share_instance):
|
||||||
host = utils.extract_host(share_instance['host'])
|
host = utils.extract_host(share_instance['host'])
|
||||||
call_context = self.client.prepare(server=host, version='1.4')
|
call_context = self.client.prepare(server=host, version='1.4')
|
||||||
|
@ -179,6 +179,18 @@ def app():
|
|||||||
mapper['/v2'] = router_v2.APIRouter()
|
mapper['/v2'] = router_v2.APIRouter()
|
||||||
return mapper
|
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 = (
|
fixture_reset_status_with_different_roles = (
|
||||||
{
|
{
|
||||||
|
@ -13,8 +13,6 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
import datetime
|
|
||||||
|
|
||||||
import ddt
|
import ddt
|
||||||
import mock
|
import mock
|
||||||
from oslo_serialization import jsonutils
|
from oslo_serialization import jsonutils
|
||||||
@ -31,6 +29,7 @@ from manila import test
|
|||||||
from manila.tests.api.contrib import stubs
|
from manila.tests.api.contrib import stubs
|
||||||
from manila.tests.api import fakes
|
from manila.tests.api import fakes
|
||||||
from manila.tests import db_utils
|
from manila.tests import db_utils
|
||||||
|
from manila.tests import fake_share
|
||||||
|
|
||||||
|
|
||||||
@ddt.ddt
|
@ddt.ddt
|
||||||
@ -77,39 +76,18 @@ class ShareSnapshotAPITest(test.TestCase):
|
|||||||
stubs.stub_snapshot_create)
|
stubs.stub_snapshot_create)
|
||||||
body = {
|
body = {
|
||||||
'snapshot': {
|
'snapshot': {
|
||||||
'share_id': 100,
|
'share_id': 'fakeshareid',
|
||||||
'force': False,
|
'force': False,
|
||||||
'name': 'fake_share_name',
|
'name': 'displaysnapname',
|
||||||
'description': 'fake_share_description',
|
'description': 'displaysnapdesc',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
req = fakes.HTTPRequest.blank('/snapshots')
|
req = fakes.HTTPRequest.blank('/snapshots')
|
||||||
|
|
||||||
res_dict = self.controller.create(req, body)
|
res_dict = self.controller.create(req, body)
|
||||||
|
|
||||||
expected = {
|
expected = fake_share.expected_snapshot(id=200)
|
||||||
'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',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.assertEqual(expected, res_dict)
|
self.assertEqual(expected, res_dict)
|
||||||
|
|
||||||
@ddt.data(0, False)
|
@ddt.data(0, False)
|
||||||
@ -162,29 +140,7 @@ class ShareSnapshotAPITest(test.TestCase):
|
|||||||
def test_snapshot_show(self):
|
def test_snapshot_show(self):
|
||||||
req = fakes.HTTPRequest.blank('/snapshots/200')
|
req = fakes.HTTPRequest.blank('/snapshots/200')
|
||||||
res_dict = self.controller.show(req, 200)
|
res_dict = self.controller.show(req, 200)
|
||||||
expected = {
|
expected = fake_share.expected_snapshot(id=200)
|
||||||
'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',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.assertEqual(expected, res_dict)
|
self.assertEqual(expected, res_dict)
|
||||||
|
|
||||||
def test_snapshot_show_nofound(self):
|
def test_snapshot_show_nofound(self):
|
||||||
@ -222,15 +178,7 @@ class ShareSnapshotAPITest(test.TestCase):
|
|||||||
self.assertEqual(expected, res_dict)
|
self.assertEqual(expected, res_dict)
|
||||||
|
|
||||||
def _snapshot_list_summary_with_search_opts(self, use_admin_context):
|
def _snapshot_list_summary_with_search_opts(self, use_admin_context):
|
||||||
search_opts = {
|
search_opts = fake_share.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',
|
|
||||||
}
|
|
||||||
# fake_key should be filtered for non-admin
|
# fake_key should be filtered for non-admin
|
||||||
url = '/snapshots?fake_key=fake_value'
|
url = '/snapshots?fake_key=fake_value'
|
||||||
for k, v in search_opts.items():
|
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)
|
self._snapshot_list_summary_with_search_opts(use_admin_context=True)
|
||||||
|
|
||||||
def _snapshot_list_detail_with_search_opts(self, use_admin_context):
|
def _snapshot_list_detail_with_search_opts(self, use_admin_context):
|
||||||
search_opts = {
|
search_opts = fake_share.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',
|
|
||||||
}
|
|
||||||
# fake_key should be filtered for non-admin
|
# fake_key should be filtered for non-admin
|
||||||
url = '/shares/detail?fake_key=fake_value'
|
url = '/shares/detail?fake_key=fake_value'
|
||||||
for k, v in search_opts.items():
|
for k, v in search_opts.items():
|
||||||
@ -348,32 +288,8 @@ class ShareSnapshotAPITest(test.TestCase):
|
|||||||
env = {'QUERY_STRING': 'name=Share+Test+Name'}
|
env = {'QUERY_STRING': 'name=Share+Test+Name'}
|
||||||
req = fakes.HTTPRequest.blank('/shares/detail', environ=env)
|
req = fakes.HTTPRequest.blank('/shares/detail', environ=env)
|
||||||
res_dict = self.controller.detail(req)
|
res_dict = self.controller.detail(req)
|
||||||
expected = {
|
expected_s = fake_share.expected_snapshot(id=2)
|
||||||
'snapshots': [
|
expected = {'snapshots': [expected_s['snapshot']]}
|
||||||
{
|
|
||||||
'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',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
self.assertEqual(expected, res_dict)
|
self.assertEqual(expected, res_dict)
|
||||||
|
|
||||||
def test_snapshot_list_status_none(self):
|
def test_snapshot_list_status_none(self):
|
||||||
@ -443,26 +359,22 @@ class ShareSnapshotAdminActionsAPITest(test.TestCase):
|
|||||||
def _get_context(self, role):
|
def _get_context(self, role):
|
||||||
return getattr(self, '%s_context' % 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:
|
if snapshot is None:
|
||||||
share = db_utils.create_share()
|
share = db_utils.create_share()
|
||||||
snapshot = db_utils.create_snapshot(
|
snapshot = db_utils.create_snapshot(
|
||||||
status=constants.STATUS_AVAILABLE, share_id=share['id'])
|
status=constants.STATUS_AVAILABLE, share_id=share['id'])
|
||||||
req = fakes.HTTPRequest.blank('/v2/fake/snapshots/%s/action' %
|
req = fakes.HTTPRequest.blank('/v1/fake/snapshots/%s/action' %
|
||||||
snapshot['id'], version=version)
|
snapshot['id'])
|
||||||
return snapshot, req
|
return snapshot, req
|
||||||
|
|
||||||
def _reset_status(self, ctxt, model, req, db_access_method,
|
def _reset_status(self, ctxt, model, req, db_access_method,
|
||||||
valid_code, valid_status=None, body=None, version='2.7'):
|
valid_code, valid_status=None, body=None):
|
||||||
if float(version) > 2.6:
|
action_name = 'os-reset_status'
|
||||||
action_name = 'reset_status'
|
|
||||||
else:
|
|
||||||
action_name = 'os-reset_status'
|
|
||||||
if body is None:
|
if body is None:
|
||||||
body = {action_name: {'status': constants.STATUS_ERROR}}
|
body = {action_name: {'status': constants.STATUS_ERROR}}
|
||||||
req.method = 'POST'
|
req.method = 'POST'
|
||||||
req.headers['content-type'] = 'application/json'
|
req.headers['content-type'] = 'application/json'
|
||||||
req.headers['X-Openstack-Manila-Api-Version'] = version
|
|
||||||
req.body = six.b(jsonutils.dumps(body))
|
req.body = six.b(jsonutils.dumps(body))
|
||||||
req.environ['manila.context'] = ctxt
|
req.environ['manila.context'] = ctxt
|
||||||
|
|
||||||
@ -480,39 +392,31 @@ class ShareSnapshotAdminActionsAPITest(test.TestCase):
|
|||||||
actual_model = db_access_method(ctxt, model['id'])
|
actual_model = db_access_method(ctxt, model['id'])
|
||||||
self.assertEqual(valid_status, actual_model['status'])
|
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
|
@ddt.unpack
|
||||||
def test_snapshot_reset_status_with_different_roles(self, role, valid_code,
|
def test_snapshot_reset_status_with_different_roles(self, role, valid_code,
|
||||||
valid_status, version):
|
valid_status):
|
||||||
ctxt = self._get_context(role)
|
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,
|
self._reset_status(ctxt, snapshot, req, db.share_snapshot_get,
|
||||||
valid_code, valid_status, version=version)
|
valid_code, valid_status)
|
||||||
|
|
||||||
@ddt.data(
|
@ddt.data(
|
||||||
({'os-reset_status': {'x-status': 'bad'}}, '2.6'),
|
{'os-reset_status': {'x-status': 'bad'}},
|
||||||
({'reset_status': {'x-status': 'bad'}}, '2.7'),
|
{'os-reset_status': {'status': 'invalid'}},
|
||||||
({'os-reset_status': {'status': 'invalid'}}, '2.6'),
|
|
||||||
({'reset_status': {'status': 'invalid'}}, '2.7'),
|
|
||||||
)
|
)
|
||||||
@ddt.unpack
|
def test_snapshot_invalid_reset_status_body(self, body):
|
||||||
def test_snapshot_invalid_reset_status_body(self, body, version):
|
snapshot, req = self._setup_snapshot_data()
|
||||||
snapshot, req = self._setup_snapshot_data(version=version)
|
|
||||||
|
|
||||||
self._reset_status(self.admin_context, snapshot, req,
|
self._reset_status(self.admin_context, snapshot, req,
|
||||||
db.share_snapshot_get, 400,
|
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,
|
def _force_delete(self, ctxt, model, req, db_access_method, valid_code):
|
||||||
version='2.7'):
|
action_name = 'os-force_delete'
|
||||||
if float(version) > 2.6:
|
|
||||||
action_name = 'force_delete'
|
|
||||||
else:
|
|
||||||
action_name = 'os-force_delete'
|
|
||||||
req.method = 'POST'
|
req.method = 'POST'
|
||||||
req.headers['content-type'] = 'application/json'
|
req.headers['content-type'] = 'application/json'
|
||||||
req.headers['X-Openstack-Manila-Api-Version'] = version
|
|
||||||
req.body = six.b(jsonutils.dumps({action_name: {}}))
|
req.body = six.b(jsonutils.dumps({action_name: {}}))
|
||||||
req.environ['manila.context'] = ctxt
|
req.environ['manila.context'] = ctxt
|
||||||
|
|
||||||
@ -521,15 +425,17 @@ class ShareSnapshotAdminActionsAPITest(test.TestCase):
|
|||||||
# Validate response
|
# Validate response
|
||||||
self.assertEqual(valid_code, resp.status_int)
|
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
|
@ddt.unpack
|
||||||
def test_snapshot_force_delete_with_different_roles(self, role, resp_code,
|
def test_snapshot_force_delete_with_different_roles(self, role, resp_code):
|
||||||
version):
|
|
||||||
ctxt = self._get_context(role)
|
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,
|
self._force_delete(ctxt, snapshot, req, db.share_snapshot_get,
|
||||||
resp_code, version=version)
|
resp_code)
|
||||||
|
|
||||||
def test_snapshot_force_delete_missing(self):
|
def test_snapshot_force_delete_missing(self):
|
||||||
ctxt = self._get_context('admin')
|
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',
|
for col_name in ('label', 'network_type', 'segmentation_id',
|
||||||
'ip_version', 'cidr'):
|
'ip_version', 'cidr'):
|
||||||
self.test_case.assertFalse(hasattr(na, col_name))
|
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,
|
'share_id': share['id'] if with_share else None,
|
||||||
'user_id': 'fake',
|
'user_id': 'fake',
|
||||||
'project_id': 'fake',
|
'project_id': 'fake',
|
||||||
'status': 'creating'
|
'status': 'creating',
|
||||||
|
'provider_location': 'fake',
|
||||||
}
|
}
|
||||||
return _create_db_row(db.share_snapshot_create, snapshot, kwargs)
|
return _create_db_row(db.share_snapshot_create, snapshot, kwargs)
|
||||||
|
|
||||||
|
@ -52,6 +52,48 @@ def fake_snapshot(**kwargs):
|
|||||||
return db_fakes.FakeModel(snapshot)
|
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):
|
def fake_access(**kwargs):
|
||||||
access = {
|
access = {
|
||||||
'id': 'fakeaccid',
|
'id': 'fakeaccid',
|
||||||
|
@ -60,6 +60,8 @@
|
|||||||
|
|
||||||
"share_snapshot:force_delete": "rule:admin_api",
|
"share_snapshot:force_delete": "rule:admin_api",
|
||||||
"share_snapshot:reset_status": "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:create": "",
|
||||||
"share_network:index": "",
|
"share_network:index": "",
|
||||||
|
@ -2013,6 +2013,46 @@ class GenericShareDriverTestCase(test.TestCase):
|
|||||||
|
|
||||||
self.assertEqual(FAKE_COLLATED_INFO, result)
|
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
|
@generic.ensure_server
|
||||||
def fake(driver_instance, context, share_server=None):
|
def fake(driver_instance, context, share_server=None):
|
||||||
|
@ -218,15 +218,23 @@ class ShareDriverTestCase(test.TestCase):
|
|||||||
share_driver.teardown_server,
|
share_driver.teardown_server,
|
||||||
'fake_share_server_details')
|
'fake_share_server_details')
|
||||||
|
|
||||||
|
def _assert_is_callable(self, obj, attr):
|
||||||
|
self.assertTrue(callable(getattr(obj, attr)))
|
||||||
|
|
||||||
@ddt.data('manage_existing',
|
@ddt.data('manage_existing',
|
||||||
'unmanage')
|
'unmanage')
|
||||||
def test_drivers_methods_needed_by_manage_functionality(self, method):
|
def test_drivers_methods_needed_by_manage_functionality(self, method):
|
||||||
share_driver = self._instantiate_share_driver(None, False)
|
share_driver = self._instantiate_share_driver(None, False)
|
||||||
|
|
||||||
def assert_is_callable(obj, attr):
|
self._assert_is_callable(share_driver, method)
|
||||||
self.assertTrue(callable(getattr(obj, attr)))
|
|
||||||
|
|
||||||
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)
|
@ddt.data(True, False)
|
||||||
def test_get_share_server_pools(self, value):
|
def test_get_share_server_pools(self, value):
|
||||||
|
@ -1240,7 +1240,7 @@ class ShareManagerTestCase(test.TestCase):
|
|||||||
|
|
||||||
def _fake_create_snapshot(self, snapshot, **kwargs):
|
def _fake_create_snapshot(self, snapshot, **kwargs):
|
||||||
snapshot['progress'] = '99%'
|
snapshot['progress'] = '99%'
|
||||||
return snapshot
|
return snapshot.to_dict()
|
||||||
|
|
||||||
self.mock_object(self.share_manager.driver, "create_snapshot",
|
self.mock_object(self.share_manager.driver, "create_snapshot",
|
||||||
_fake_create_snapshot)
|
_fake_create_snapshot)
|
||||||
@ -1249,7 +1249,6 @@ class ShareManagerTestCase(test.TestCase):
|
|||||||
share_id = share['id']
|
share_id = share['id']
|
||||||
snapshot = db_utils.create_snapshot(share_id=share_id)
|
snapshot = db_utils.create_snapshot(share_id=share_id)
|
||||||
snapshot_id = snapshot['id']
|
snapshot_id = snapshot['id']
|
||||||
|
|
||||||
self.share_manager.create_snapshot(self.context, share_id,
|
self.share_manager.create_snapshot(self.context, share_id,
|
||||||
snapshot_id)
|
snapshot_id)
|
||||||
self.assertEqual(share_id,
|
self.assertEqual(share_id,
|
||||||
@ -3614,6 +3613,220 @@ class ShareManagerTestCase(test.TestCase):
|
|||||||
manager._migrate_share_generic,
|
manager._migrate_share_generic,
|
||||||
self.context, share, host)
|
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
|
@ddt.ddt
|
||||||
class HookWrapperTestCase(test.TestCase):
|
class HookWrapperTestCase(test.TestCase):
|
||||||
|
@ -280,6 +280,21 @@ class ShareRpcAPITestCase(test.TestCase):
|
|||||||
version='1.8',
|
version='1.8',
|
||||||
share_replica=self.fake_share_replica)
|
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):
|
class Desthost(object):
|
||||||
host = 'fake_host'
|
host = 'fake_host'
|
||||||
capabilities = 1
|
capabilities = 1
|
||||||
|
@ -227,6 +227,20 @@ class ManilaExceptionResponseCode400(test.TestCase):
|
|||||||
self.assertEqual(400, e.code)
|
self.assertEqual(400, e.code)
|
||||||
self.assertIn(reason, e.msg)
|
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):
|
class ManilaExceptionResponseCode403(test.TestCase):
|
||||||
|
|
||||||
@ -490,6 +504,13 @@ class ManilaExceptionResponseCode404(test.TestCase):
|
|||||||
self.assertEqual(404, e.code)
|
self.assertEqual(404, e.code)
|
||||||
self.assertIn(share_id, e.msg)
|
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):
|
class ManilaExceptionResponseCode413(test.TestCase):
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@ ShareGroup = [
|
|||||||
help="The minimum api microversion is configured to be the "
|
help="The minimum api microversion is configured to be the "
|
||||||
"value of the minimum microversion supported by Manila."),
|
"value of the minimum microversion supported by Manila."),
|
||||||
cfg.StrOpt("max_api_microversion",
|
cfg.StrOpt("max_api_microversion",
|
||||||
default="2.11",
|
default="2.12",
|
||||||
help="The maximum api microversion is configured to be the "
|
help="The maximum api microversion is configured to be the "
|
||||||
"value of the latest microversion supported by Manila."),
|
"value of the latest microversion supported by Manila."),
|
||||||
cfg.StrOpt("region",
|
cfg.StrOpt("region",
|
||||||
@ -128,11 +128,6 @@ ShareGroup = [
|
|||||||
help="Whether to suppress errors with clean up operation "
|
help="Whether to suppress errors with clean up operation "
|
||||||
"or not. There are cases when we may want to skip "
|
"or not. There are cases when we may want to skip "
|
||||||
"such errors and catch only test errors."),
|
"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
|
# Switching ON/OFF test suites filtered by features
|
||||||
cfg.BoolOpt("run_quota_tests",
|
cfg.BoolOpt("run_quota_tests",
|
||||||
@ -161,6 +156,16 @@ ShareGroup = [
|
|||||||
cfg.BoolOpt("run_migration_tests",
|
cfg.BoolOpt("run_migration_tests",
|
||||||
default=False,
|
default=False,
|
||||||
help="Enable or disable migration tests."),
|
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",
|
cfg.StrOpt("image_with_share_tools",
|
||||||
default="manila-service-image",
|
default="manila-service-image",
|
||||||
|
@ -437,6 +437,113 @@ class SharesV2Client(shares_client.SharesClient):
|
|||||||
self.expected_success(202, resp.status)
|
self.expected_success(202, resp.status)
|
||||||
return body
|
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):
|
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_not_supported = utils.skip_if_microversion_not_supported
|
||||||
|
skip_if_microversion_lt = utils.skip_if_microversion_lt
|
||||||
|
|
||||||
|
|
||||||
class BaseSharesTest(test.BaseTestCase):
|
class BaseSharesTest(test.BaseTestCase):
|
||||||
@ -104,6 +105,13 @@ class BaseSharesTest(test.BaseTestCase):
|
|||||||
raise self.skipException(
|
raise self.skipException(
|
||||||
"Microversion '%s' is not supported." % microversion)
|
"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
|
@classmethod
|
||||||
def get_client_with_isolated_creds(cls,
|
def get_client_with_isolated_creds(cls,
|
||||||
name=None,
|
name=None,
|
||||||
|
@ -81,6 +81,15 @@ def skip_if_microversion_not_supported(microversion):
|
|||||||
return lambda f: f
|
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():
|
def rand_ip():
|
||||||
"""This uses the TEST-NET-3 range of reserved IP addresses.
|
"""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