Add mountable snapshots support

This new feature gives the user the ability to allow and
deny access to the snapshots, so that they could be mounted in
read-only mode to retrieve files.

APIImpact
DocImpact

Co-Authored-By: Rodrigo Barbieri <rodrigo.barbieri@fit-tecnologia.org.br>
Co-Authored-By: Alyson Rosa <alyson.rosa@fit-tecnologia.org.br>
Co-Authored-By: Miriam Yumi <miriam.peixoto@fit-tecnologia.org.br>

Partially-implements: blueprint manila-mountable-snapshots
Change-Id: I65f398a05f82eef31ec317d70dfa101483b44b30
This commit is contained in:
tpsilva 2016-07-08 14:41:35 -03:00 committed by Rodrigo Barbieri
parent c6d2ae6492
commit 8d71932c69
63 changed files with 3390 additions and 148 deletions

View File

@ -76,6 +76,7 @@ RUN_MANILA_MANAGE_SNAPSHOT_TESTS=${RUN_MANILA_MANAGE_SNAPSHOT_TESTS:-False}
RUN_MANILA_REPLICATION_TESTS=${RUN_MANILA_REPLICATION_TESTS:-False}
RUN_MANILA_HOST_ASSISTED_MIGRATION_TESTS=${RUN_MANILA_HOST_ASSISTED_MIGRATION_TESTS:-False}
RUN_MANILA_DRIVER_ASSISTED_MIGRATION_TESTS=${RUN_MANILA_DRIVER_ASSISTED_MIGRATION_TESTS:-False}
RUN_MANILA_MOUNT_SNAPSHOT_TESTS=${RUN_MANILA_MOUNT_SNAPSHOT_TESTS:-False}
MANILA_CONF=${MANILA_CONF:-/etc/manila/manila.conf}
@ -167,6 +168,7 @@ if [[ "$DRIVER" == "lvm" ]]; then
RUN_MANILA_HOST_ASSISTED_MIGRATION_TESTS=True
RUN_MANILA_SHRINK_TESTS=False
RUN_MANILA_REVERT_TO_SNAPSHOT_TESTS=True
RUN_MANILA_MOUNT_SNAPSHOT_TESTS=True
iniset $TEMPEST_CONFIG share enable_ip_rules_for_protocols 'nfs'
iniset $TEMPEST_CONFIG share enable_user_rules_for_protocols 'cifs'
iniset $TEMPEST_CONFIG share image_with_share_tools 'manila-service-image-master'
@ -211,6 +213,7 @@ elif [[ "$DRIVER" == "dummy" ]]; then
RUN_MANILA_MANAGE_TESTS=False
RUN_MANILA_DRIVER_ASSISTED_MIGRATION_TESTS=True
RUN_MANILA_REVERT_TO_SNAPSHOT_TESTS=True
RUN_MANILA_MOUNT_SNAPSHOT_TESTS=True
iniset $TEMPEST_CONFIG share enable_ip_rules_for_protocols 'nfs'
iniset $TEMPEST_CONFIG share enable_user_rules_for_protocols 'cifs'
iniset $TEMPEST_CONFIG share enable_cert_rules_for_protocols ''
@ -266,6 +269,9 @@ iniset $TEMPEST_CONFIG share run_replication_tests $RUN_MANILA_REPLICATION_TESTS
iniset $TEMPEST_CONFIG share run_host_assisted_migration_tests $RUN_MANILA_HOST_ASSISTED_MIGRATION_TESTS
iniset $TEMPEST_CONFIG share run_driver_assisted_migration_tests $RUN_MANILA_DRIVER_ASSISTED_MIGRATION_TESTS
# Enable mountable snapshots tests
iniset $TEMPEST_CONFIG share run_mount_snapshot_tests $RUN_MANILA_MOUNT_SNAPSHOT_TESTS
# Create share from snapshot support
iniset $TEMPEST_CONFIG share capability_create_share_from_snapshot_support $CAPABILITY_CREATE_SHARE_FROM_SNAPSHOT_SUPPORT

View File

@ -99,7 +99,7 @@ elif [[ "$DRIVER" == "windows" ]]; then
save_configuration "SHARE_DRIVER" "manila.share.drivers.windows.windows_smb_driver.WindowsSMBDriver"
elif [[ "$DRIVER" == "dummy" ]]; then
driver_path="manila.tests.share.drivers.dummy.DummyDriver"
DEFAULT_EXTRA_SPECS="'snapshot_support=True create_share_from_snapshot_support=True revert_to_snapshot_support=True'"
DEFAULT_EXTRA_SPECS="'snapshot_support=True create_share_from_snapshot_support=True revert_to_snapshot_support=True mount_snapshot_support=True'"
save_configuration "MANILA_SERVICE_IMAGE_ENABLED" "False"
save_configuration "SHARE_DRIVER" "$driver_path"
save_configuration "SUPPRESS_ERRORS_IN_CLEANUP" "False"
@ -149,7 +149,7 @@ elif [[ "$DRIVER" == "dummy" ]]; then
elif [[ "$DRIVER" == "lvm" ]]; then
MANILA_SERVICE_IMAGE_ENABLED=True
DEFAULT_EXTRA_SPECS="'snapshot_support=True create_share_from_snapshot_support=True revert_to_snapshot_support=True'"
DEFAULT_EXTRA_SPECS="'snapshot_support=True create_share_from_snapshot_support=True revert_to_snapshot_support=True mount_snapshot_support=True'"
save_configuration "SHARE_DRIVER" "manila.share.drivers.lvm.LVMShareDriver"
save_configuration "SHARE_BACKING_FILE_SIZE" "32000M"
elif [[ "$DRIVER" == "zfsonlinux" ]]; then

View File

@ -61,11 +61,18 @@
"share_snapshot:unmanage_snapshot": "rule:admin_api",
"share_snapshot:force_delete": "rule:admin_api",
"share_snapshot:reset_status": "rule:admin_api",
"share_snapshot:access_list": "rule:default",
"share_snapshot:allow_access": "rule:default",
"share_snapshot:deny_access": "rule:default",
"share_snapshot_export_location:index": "rule:default",
"share_snapshot_export_location:show": "rule:default",
"share_snapshot_instance:detail": "rule:admin_api",
"share_snapshot_instance:index": "rule:admin_api",
"share_snapshot_instance:show": "rule:admin_api",
"share_snapshot_instance:reset_status": "rule:admin_api",
"share_snapshot_instance_export_location:index": "rule:admin_api",
"share_snapshot_instance_export_location:show": "rule:admin_api",
"share_type:index": "rule:default",
"share_type:show": "rule:default",

View File

@ -15,6 +15,7 @@
import os
import re
import string
from oslo_config import cfg
from oslo_log import log
@ -316,3 +317,98 @@ def remove_invalid_options(context, search_options, allowed_search_options):
{"bad_options": bad_options})
for opt in unknown_options:
del search_options[opt]
def validate_common_name(access):
"""Validate common name passed by user.
'access' is used as the certificate's CN (common name)
to which access is allowed or denied by the backend.
The standard allows for just about any string in the
common name. The meaning of a string depends on its
interpretation and is limited to 64 characters.
"""
if not(0 < len(access) < 65):
exc_str = _('Invalid CN (common name). Must be 1-64 chars long.')
raise webob.exc.HTTPBadRequest(explanation=exc_str)
def validate_username(access):
valid_username_re = '[\w\.\-_\`;\'\{\}\[\]\\\\]{4,32}$'
username = access
if not re.match(valid_username_re, username):
exc_str = ('Invalid user or group name. Must be 4-32 characters '
'and consist of alphanumeric characters and '
'special characters ]{.-_\'`;}[\\')
raise webob.exc.HTTPBadRequest(explanation=exc_str)
def validate_ip_range(ip_range):
ip_range = ip_range.split('/')
exc_str = ('Supported ip format examples:\n'
'\t10.0.0.2, 10.0.0.0/24')
if len(ip_range) > 2:
raise webob.exc.HTTPBadRequest(explanation=exc_str)
if len(ip_range) == 2:
try:
prefix = int(ip_range[1])
if prefix < 0 or prefix > 32:
raise ValueError()
except ValueError:
msg = 'IP prefix should be in range from 0 to 32.'
raise webob.exc.HTTPBadRequest(explanation=msg)
ip_range = ip_range[0].split('.')
if len(ip_range) != 4:
raise webob.exc.HTTPBadRequest(explanation=exc_str)
for item in ip_range:
try:
if 0 <= int(item) <= 255:
continue
raise ValueError()
except ValueError:
raise webob.exc.HTTPBadRequest(explanation=exc_str)
def validate_cephx_id(cephx_id):
if not cephx_id:
raise webob.exc.HTTPBadRequest(explanation=_(
'Ceph IDs may not be empty.'))
# This restriction may be lifted in Ceph in the future:
# http://tracker.ceph.com/issues/14626
if not set(cephx_id) <= set(string.printable):
raise webob.exc.HTTPBadRequest(explanation=_(
'Ceph IDs must consist of ASCII printable characters.'))
# Periods are technically permitted, but we restrict them here
# to avoid confusion where users are unsure whether they should
# include the "client." prefix: otherwise they could accidentally
# create "client.client.foobar".
if '.' in cephx_id:
raise webob.exc.HTTPBadRequest(explanation=_(
'Ceph IDs may not contain periods.'))
def validate_access(*args, **kwargs):
access_type = kwargs.get('access_type')
access_to = kwargs.get('access_to')
enable_ceph = kwargs.get('enable_ceph')
if access_type == 'ip':
validate_ip_range(access_to)
elif access_type == 'user':
validate_username(access_to)
elif access_type == 'cert':
validate_common_name(access_to.strip())
elif access_type == "cephx" and enable_ceph:
validate_cephx_id(access_to)
else:
if enable_ceph:
exc_str = _("Only 'ip', 'user', 'cert' or 'cephx' access "
"types are supported.")
else:
exc_str = _("Only 'ip', 'user' or 'cert' access types "
"are supported.")
raise webob.exc.HTTPBadRequest(explanation=exc_str)

View File

@ -97,13 +97,14 @@ REST_API_VERSION_HISTORY = """
unsupported.
* 2.30 - Added cast_rules_to_readonly field to share_instances.
* 2.31 - Convert consistency groups to share groups.
* 2.32 - Added mountable snapshots APIs.
"""
# The minimum and maximum versions of the API supported
# The default api version request is defined to be the
# minimum version of the API supported.
_MIN_API_VERSION = "2.0"
_MAX_API_VERSION = "2.31"
_MAX_API_VERSION = "2.32"
DEFAULT_API_VERSION = _MIN_API_VERSION

View File

@ -187,3 +187,7 @@ user documentation.
2.31
----
Convert consistency groups to share groups.
2.32
----
Added mountable snapshots APIs.

View File

@ -16,8 +16,6 @@
"""The shares api."""
import ast
import re
import string
from oslo_log import log
from oslo_utils import strutils
@ -332,76 +330,6 @@ class ShareMixin(object):
return self._view_builder.detail(req, new_share)
@staticmethod
def _validate_common_name(access):
"""Validate common name passed by user.
'access' is used as the certificate's CN (common name)
to which access is allowed or denied by the backend.
The standard allows for just about any string in the
common name. The meaning of a string depends on its
interpretation and is limited to 64 characters.
"""
if len(access) == 0 or len(access) > 64:
exc_str = _('Invalid CN (common name). Must be 1-64 chars long')
raise webob.exc.HTTPBadRequest(explanation=exc_str)
@staticmethod
def _validate_username(access):
valid_username_re = '[\w\.\-_\`;\'\{\}\[\]\\\\]{4,32}$'
username = access
if not re.match(valid_username_re, username):
exc_str = ('Invalid user or group name. Must be 4-32 characters '
'and consist of alphanumeric characters and '
'special characters ]{.-_\'`;}[\\')
raise webob.exc.HTTPBadRequest(explanation=exc_str)
@staticmethod
def _validate_ip_range(ip_range):
ip_range = ip_range.split('/')
exc_str = ('Supported ip format examples:\n'
'\t10.0.0.2, 10.0.0.0/24')
if len(ip_range) > 2:
raise webob.exc.HTTPBadRequest(explanation=exc_str)
if len(ip_range) == 2:
try:
prefix = int(ip_range[1])
if prefix < 0 or prefix > 32:
raise ValueError()
except ValueError:
msg = 'IP prefix should be in range from 0 to 32'
raise webob.exc.HTTPBadRequest(explanation=msg)
ip_range = ip_range[0].split('.')
if len(ip_range) != 4:
raise webob.exc.HTTPBadRequest(explanation=exc_str)
for item in ip_range:
try:
if 0 <= int(item) <= 255:
continue
raise ValueError()
except ValueError:
raise webob.exc.HTTPBadRequest(explanation=exc_str)
@staticmethod
def _validate_cephx_id(cephx_id):
if not cephx_id:
raise webob.exc.HTTPBadRequest(explanation=_(
'Ceph IDs may not be empty'))
# This restriction may be lifted in Ceph in the future:
# http://tracker.ceph.com/issues/14626
if not set(cephx_id) <= set(string.printable):
raise webob.exc.HTTPBadRequest(explanation=_(
'Ceph IDs must consist of ASCII printable characters'))
# Periods are technically permitted, but we restrict them here
# to avoid confusion where users are unsure whether they should
# include the "client." prefix: otherwise they could accidentally
# create "client.client.foobar".
if '.' in cephx_id:
raise webob.exc.HTTPBadRequest(explanation=_(
'Ceph IDs may not contain periods'))
@staticmethod
def _any_instance_has_errored_rules(share):
for instance in share['instances']:
@ -432,23 +360,9 @@ class ShareMixin(object):
access_type = access_data['access_type']
access_to = access_data['access_to']
if access_type == 'ip':
self._validate_ip_range(access_to)
elif access_type == 'user':
self._validate_username(access_to)
elif access_type == 'cert':
self._validate_common_name(access_to.strip())
elif access_type == "cephx" and enable_ceph:
self._validate_cephx_id(access_to)
else:
if enable_ceph:
exc_str = _("Only 'ip', 'user', 'cert' or 'cephx' access "
"types are supported.")
else:
exc_str = _("Only 'ip', 'user' or 'cert' access types "
"are supported.")
raise webob.exc.HTTPBadRequest(explanation=exc_str)
common.validate_access(access_type=access_type,
access_to=access_to,
enable_ceph=enable_ceph)
try:
access = self.share_api.allow_access(
context, share, access_type, access_to,

View File

@ -43,6 +43,8 @@ from manila.api.v2 import share_instance_export_locations
from manila.api.v2 import share_instances
from manila.api.v2 import share_networks
from manila.api.v2 import share_replicas
from manila.api.v2 import share_snapshot_export_locations
from manila.api.v2 import share_snapshot_instance_export_locations
from manila.api.v2 import share_snapshot_instances
from manila.api.v2 import share_snapshots
from manila.api.v2 import share_types
@ -209,6 +211,30 @@ class APIRouter(manila.api.openstack.APIRouter):
action="manage",
conditions={"method": ["POST"]})
mapper.connect("snapshots",
"/{project_id}/snapshots/{snapshot_id}/access-list",
controller=self.resources["snapshots"],
action="access_list",
conditions={"method": ["GET"]})
self.resources["share_snapshot_export_locations"] = (
share_snapshot_export_locations.create_resource())
mapper.connect("snapshots",
"/{project_id}/snapshots/{snapshot_id}/"
"export-locations",
controller=self.resources[
"share_snapshot_export_locations"],
action="index",
conditions={"method": ["GET"]})
mapper.connect("snapshots",
"/{project_id}/snapshots/{snapshot_id}/"
"export-locations/{export_location_id}",
controller=self.resources[
"share_snapshot_export_locations"],
action="show",
conditions={"method": ["GET"]})
self.resources['snapshot_instances'] = (
share_snapshot_instances.create_resource())
mapper.resource("snapshot-instance", "snapshot-instances",
@ -216,6 +242,25 @@ class APIRouter(manila.api.openstack.APIRouter):
collection={'detail': 'GET'},
member={'action': 'POST'})
self.resources["share_snapshot_instance_export_locations"] = (
share_snapshot_instance_export_locations.create_resource())
mapper.connect("snapshot-instance",
"/{project_id}/snapshot-instances/"
"{snapshot_instance_id}/export-locations",
controller=self.resources[
"share_snapshot_instance_export_locations"],
action="index",
conditions={"method": ["GET"]})
mapper.connect("snapshot-instance",
"/{project_id}/snapshot-instances/"
"{snapshot_instance_id}/export-locations/"
"{export_location_id}",
controller=self.resources[
"share_snapshot_instance_export_locations"],
action="show",
conditions={"method": ["GET"]})
self.resources["share_metadata"] = share_metadata.create_resource()
share_metadata_controller = self.resources["share_metadata"]

View File

@ -0,0 +1,65 @@
# Copyright (c) 2016 Hitachi Data Systems
# 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.
from webob import exc
from manila.api.openstack import wsgi
from manila.api.views import share_snapshot_export_locations
from manila.db import api as db_api
from manila import exception
from manila.i18n import _
from manila import policy
class ShareSnapshotExportLocationController(wsgi.Controller):
def __init__(self):
self._view_builder_class = (
share_snapshot_export_locations.ViewBuilder)
self.resource_name = 'share_snapshot_export_location'
super(self.__class__, self).__init__()
@wsgi.Controller.api_version('2.32')
@wsgi.Controller.authorize
def index(self, req, snapshot_id):
context = req.environ['manila.context']
snapshot = self._verify_snapshot(context, snapshot_id)
return self._view_builder.list_export_locations(
req, snapshot['export_locations'])
@wsgi.Controller.api_version('2.32')
@wsgi.Controller.authorize
def show(self, req, snapshot_id, export_location_id):
context = req.environ['manila.context']
self._verify_snapshot(context, snapshot_id)
export_location = db_api.share_snapshot_instance_export_location_get(
context, export_location_id)
return self._view_builder.detail_export_location(req, export_location)
def _verify_snapshot(self, context, snapshot_id):
try:
snapshot = db_api.share_snapshot_get(context, snapshot_id)
share = db_api.share_get(context, snapshot['share_id'])
if not share['is_public']:
policy.check_policy(context, 'share', 'get', share)
except exception.NotFound:
msg = _("Snapshot '%s' not found.") % snapshot_id
raise exc.HTTPNotFound(explanation=msg)
return snapshot
def create_resource():
return wsgi.Resource(ShareSnapshotExportLocationController())

View File

@ -0,0 +1,70 @@
# Copyright (c) 2016 Hitachi Data Systems
# 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.
from webob import exc
from manila.api.openstack import wsgi
from manila.api.views import share_snapshot_export_locations
from manila.db import api as db_api
from manila import exception
from manila.i18n import _
from manila import policy
class ShareSnapshotInstanceExportLocationController(wsgi.Controller):
def __init__(self):
self._view_builder_class = (
share_snapshot_export_locations.ViewBuilder)
self.resource_name = 'share_snapshot_instance_export_location'
super(self.__class__, self).__init__()
@wsgi.Controller.api_version('2.32')
@wsgi.Controller.authorize
def index(self, req, snapshot_instance_id):
context = req.environ['manila.context']
instance = self._verify_snapshot_instance(
context, snapshot_instance_id)
export_locations = (
db_api.share_snapshot_instance_export_locations_get_all(
context, instance['id']))
return self._view_builder.list_export_locations(req, export_locations)
@wsgi.Controller.api_version('2.32')
@wsgi.Controller.authorize
def show(self, req, snapshot_instance_id, export_location_id):
context = req.environ['manila.context']
self._verify_snapshot_instance(context, snapshot_instance_id)
export_location = db_api.share_snapshot_instance_export_location_get(
context, export_location_id)
return self._view_builder.detail_export_location(req, export_location)
def _verify_snapshot_instance(self, context, snapshot_instance_id):
try:
snapshot_instance = db_api.share_snapshot_instance_get(
context, snapshot_instance_id)
share = db_api.share_get(
context, snapshot_instance.share_instance['share_id'])
if not share['is_public']:
policy.check_policy(context, 'share', 'get', share)
except exception.NotFound:
msg = _("Snapshot instance '%s' not found.") % snapshot_instance_id
raise exc.HTTPNotFound(explanation=msg)
return snapshot_instance
def create_resource():
return wsgi.Resource(ShareSnapshotInstanceExportLocationController())

View File

@ -21,6 +21,7 @@ import six
import webob
from webob import exc
from manila.api import common
from manila.api.openstack import wsgi
from manila.api.v1 import share_snapshots
from manila.api.views import share_snapshots as snapshot_views
@ -134,19 +135,104 @@ class ShareSnapshotsController(share_snapshots.ShareSnapshotMixin,
msg = _("Snapshot entity not found in request body.")
raise exc.HTTPUnprocessableEntity(explanation=msg)
required_parameters = ('share_id', 'provider_location')
data = body['snapshot']
required_parameters = ('share_id', 'provider_location')
self._validate_parameters(data, required_parameters)
return data
def _validate_parameters(self, data, required_parameters,
fix_response=False):
if fix_response:
exc_response = exc.HTTPBadRequest
else:
exc_response = exc.HTTPUnprocessableEntity
for parameter in required_parameters:
if parameter not in data:
msg = _("Required parameter %s not found.") % parameter
raise exc.HTTPUnprocessableEntity(explanation=msg)
raise exc_response(explanation=msg)
if not data.get(parameter):
msg = _("Required parameter %s is empty.") % parameter
raise exc.HTTPUnprocessableEntity(explanation=msg)
raise exc_response(explanation=msg)
return data
def _allow(self, req, id, body):
context = req.environ['manila.context']
if not (body and self.is_valid_body(body, 'allow_access')):
msg = _("Access data not found in request body.")
raise exc.HTTPBadRequest(explanation=msg)
access_data = body.get('allow_access')
required_parameters = ('access_type', 'access_to')
self._validate_parameters(access_data, required_parameters,
fix_response=True)
access_type = access_data['access_type']
access_to = access_data['access_to']
common.validate_access(access_type=access_type, access_to=access_to)
snapshot = self.share_api.get_snapshot(context, id)
self._check_mount_snapshot_support(context, snapshot)
try:
access = self.share_api.snapshot_allow_access(
context, snapshot, access_type, access_to)
except exception.ShareSnapshotAccessExists as e:
raise webob.exc.HTTPBadRequest(explanation=e.msg)
return self._view_builder.detail_access(req, access)
def _deny(self, req, id, body):
context = req.environ['manila.context']
if not (body and self.is_valid_body(body, 'deny_access')):
msg = _("Access data not found in request body.")
raise exc.HTTPBadRequest(explanation=msg)
access_data = body.get('deny_access')
self._validate_parameters(
access_data, ('access_id',), fix_response=True)
access_id = access_data['access_id']
snapshot = self.share_api.get_snapshot(context, id)
self._check_mount_snapshot_support(context, snapshot)
access = self.share_api.snapshot_access_get(context, access_id)
if access['share_snapshot_id'] != snapshot['id']:
msg = _("Access rule provided is not associated with given"
" snapshot.")
raise webob.exc.HTTPBadRequest(explanation=msg)
self.share_api.snapshot_deny_access(context, snapshot, access)
return webob.Response(status_int=202)
def _check_mount_snapshot_support(self, context, snapshot):
share = self.share_api.get(context, snapshot['share_id'])
if not share['mount_snapshot_support']:
msg = _("Cannot control access to the snapshot %(snap)s since the "
"parent share %(share)s does not support mounting its "
"snapshots.") % {'snap': snapshot['id'],
'share': share['id']}
raise exc.HTTPBadRequest(explanation=msg)
def _access_list(self, req, snapshot_id):
context = req.environ['manila.context']
snapshot = self.share_api.get_snapshot(context, snapshot_id)
self._check_mount_snapshot_support(context, snapshot)
access_list = self.share_api.snapshot_access_get_all(context, snapshot)
return self._view_builder.detail_list_access(req, access_list)
@wsgi.Controller.api_version('2.0', '2.6')
@wsgi.action('os-reset_status')
@ -178,6 +264,24 @@ class ShareSnapshotsController(share_snapshots.ShareSnapshotMixin,
def unmanage(self, req, id, body=None):
return self._unmanage(req, id, body)
@wsgi.Controller.api_version('2.32')
@wsgi.action('allow_access')
@wsgi.response(202)
@wsgi.Controller.authorize
def allow_access(self, req, id, body=None):
return self._allow(req, id, body)
@wsgi.Controller.api_version('2.32')
@wsgi.action('deny_access')
@wsgi.Controller.authorize
def deny_access(self, req, id, body=None):
return self._deny(req, id, body)
@wsgi.Controller.api_version('2.32')
@wsgi.Controller.authorize
def access_list(self, req, snapshot_id):
return self._access_list(req, snapshot_id)
def create_resource():
return wsgi.Resource(ShareSnapshotsController())

View File

@ -0,0 +1,61 @@
# Copyright (c) 2016 Hitachi Data Systems
# 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.
from manila.api import common
class ViewBuilder(common.ViewBuilder):
_collection_name = "share_snapshot_export_locations"
def _get_view(self, request, export_location, detail=False):
context = request.environ['manila.context']
result = {
'share_snapshot_export_location': {
'id': export_location['id'],
'path': export_location['path'],
'links': self._get_links(request, export_location['id']),
}
}
ss_el = result['share_snapshot_export_location']
if context.is_admin:
ss_el['share_snapshot_instance_id'] = (
export_location['share_snapshot_instance_id'])
ss_el['is_admin_only'] = export_location['is_admin_only']
if detail:
ss_el['created_at'] = export_location['created_at']
ss_el['updated_at'] = export_location['updated_at']
return result
def list_export_locations(self, request, export_locations):
context = request.environ['manila.context']
result = {self._collection_name: []}
for export_location in export_locations:
if context.is_admin or not export_location['is_admin_only']:
result[self._collection_name].append(self._get_view(
request,
export_location)['share_snapshot_export_location'])
else:
continue
return result
def detail_export_location(self, request, export_location):
return self._get_view(request, export_location, detail=True)

View File

@ -87,3 +87,21 @@ class ViewBuilder(common.ViewBuilder):
snapshots_dict['share_snapshots_links'] = snapshots_links
return snapshots_dict
def detail_access(self, request, access):
access = {
'snapshot_access': {
'id': access['id'],
'access_type': access['access_type'],
'access_to': access['access_to'],
'state': access['state'],
}
}
return access
def detail_list_access(self, request, access_list):
return {
'snapshot_access_list':
([self.detail_access(request, access)['snapshot_access']
for access in access_list])
}

View File

@ -33,6 +33,7 @@ class ViewBuilder(common.ViewBuilder):
"add_revert_to_snapshot_support_field",
"translate_access_rules_status",
"add_share_group_fields",
"add_mount_snapshot_support_field",
]
def summary_list(self, request, shares):
@ -162,6 +163,11 @@ class ViewBuilder(common.ViewBuilder):
share_dict['source_share_group_snapshot_member_id'] = share.get(
'source_share_group_snapshot_member_id')
@common.ViewBuilder.versioned_method("2.32")
def add_mount_snapshot_support_field(self, context, share_dict, share):
share_dict['mount_snapshot_support'] = share.get(
'mount_snapshot_support')
def _list_view(self, func, request, shares):
"""Provide a view for a list of shares."""
shares_list = [func(request, share)['share'] for share in shares]

View File

@ -47,6 +47,7 @@ ACCESS_STATE_APPLYING = 'applying'
ACCESS_STATE_DENYING = 'denying'
ACCESS_STATE_ACTIVE = 'active'
ACCESS_STATE_ERROR = 'error'
ACCESS_STATE_DELETED = 'deleted'
# Share instance "access_rules_status" field values
SHARE_INSTANCE_RULES_SYNCING = 'syncing'
@ -57,6 +58,16 @@ STATUS_NEW = 'new'
STATUS_OUT_OF_SYNC = 'out_of_sync'
STATUS_ACTIVE = 'active'
ACCESS_RULES_STATES = (
ACCESS_STATE_QUEUED_TO_APPLY,
ACCESS_STATE_QUEUED_TO_DENY,
ACCESS_STATE_APPLYING,
ACCESS_STATE_DENYING,
ACCESS_STATE_ACTIVE,
ACCESS_STATE_ERROR,
ACCESS_STATE_DELETED,
)
TASK_STATE_MIGRATION_STARTING = 'migration_starting'
TASK_STATE_MIGRATION_IN_PROGRESS = 'migration_in_progress'
TASK_STATE_MIGRATION_COMPLETING = 'migration_completing'
@ -182,6 +193,7 @@ class ExtraSpecs(object):
REPLICATION_TYPE_SPEC = "replication_type"
CREATE_SHARE_FROM_SNAPSHOT_SUPPORT = "create_share_from_snapshot_support"
REVERT_TO_SNAPSHOT_SUPPORT = "revert_to_snapshot_support"
MOUNT_SNAPSHOT_SUPPORT = "mount_snapshot_support"
# Extra specs containers
REQUIRED = (
@ -193,6 +205,7 @@ class ExtraSpecs(object):
CREATE_SHARE_FROM_SNAPSHOT_SUPPORT,
REVERT_TO_SNAPSHOT_SUPPORT,
REPLICATION_TYPE_SPEC,
MOUNT_SNAPSHOT_SUPPORT,
)
# NOTE(cknight): Some extra specs are necessary parts of the Manila API and
@ -205,6 +218,7 @@ class ExtraSpecs(object):
SNAPSHOT_SUPPORT,
CREATE_SHARE_FROM_SNAPSHOT_SUPPORT,
REVERT_TO_SNAPSHOT_SUPPORT,
MOUNT_SNAPSHOT_SUPPORT,
)
# NOTE(cknight): Some extra specs are optional, but a nominal (typically
@ -214,6 +228,7 @@ class ExtraSpecs(object):
SNAPSHOT_SUPPORT: False,
CREATE_SHARE_FROM_SNAPSHOT_SUPPORT: False,
REVERT_TO_SNAPSHOT_SUPPORT: False,
MOUNT_SNAPSHOT_SUPPORT: False,
}
REPLICATION_TYPES = ('writable', 'readable', 'dr')

View File

@ -538,6 +538,81 @@ def share_snapshot_update(context, snapshot_id, values):
return IMPL.share_snapshot_update(context, snapshot_id, values)
###################
def share_snapshot_access_create(context, values):
"""Create a share snapshot access from the values dictionary."""
return IMPL.share_snapshot_access_create(context, values)
def share_snapshot_access_get(context, access_id):
"""Get share snapshot access rule from given access_id."""
return IMPL.share_snapshot_access_get(context, access_id)
def share_snapshot_access_get_all_for_snapshot_instance(
context, snapshot_instance_id, session=None):
"""Get all access rules related to a certain snapshot instance."""
return IMPL.share_snapshot_access_get_all_for_snapshot_instance(
context, snapshot_instance_id, session)
def share_snapshot_access_get_all_for_share_snapshot(context,
share_snapshot_id,
filters):
"""Get all access rules for a given share snapshot according to filters."""
return IMPL.share_snapshot_access_get_all_for_share_snapshot(
context, share_snapshot_id, filters)
def share_snapshot_export_locations_get(context, snapshot_id):
"""Get all export locations for a given share snapshot."""
return IMPL.share_snapshot_export_locations_get(context, snapshot_id)
def share_snapshot_instance_access_update(
context, access_id, instance_id, updates):
"""Update the state of the share snapshot instance access."""
return IMPL.share_snapshot_instance_access_update(
context, access_id, instance_id, updates)
def share_snapshot_instance_access_get(context, share_snapshot_instance_id,
access_id):
"""Get the share snapshot instance access related to given ids."""
return IMPL.share_snapshot_instance_access_get(
context, share_snapshot_instance_id, access_id)
def share_snapshot_instance_access_delete(context, access_id,
snapshot_instance_id):
"""Delete share snapshot instance access given its id."""
return IMPL.share_snapshot_instance_access_delete(
context, access_id, snapshot_instance_id)
def share_snapshot_instance_export_location_create(context, values):
"""Create a share snapshot instance export location."""
return IMPL.share_snapshot_instance_export_location_create(context, values)
def share_snapshot_instance_export_locations_get_all(
context, share_snapshot_instance_id):
"""Get the share snapshot instance export locations for given id."""
return IMPL.share_snapshot_instance_export_locations_get_all(
context, share_snapshot_instance_id)
def share_snapshot_instance_export_location_get(context, el_id):
"""Get the share snapshot instance export location for given id."""
return IMPL.share_snapshot_instance_export_location_get(
context, el_id)
def share_snapshot_instance_export_location_delete(context, el_id):
"""Delete share snapshot instance export location given its id."""
return IMPL.share_snapshot_instance_export_location_delete(context, el_id)
###################
def security_service_create(context, values):
"""Create security service DB record."""

View File

@ -0,0 +1,101 @@
# Copyright (c) 2016 Hitachi Data Systems.
# 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.
"""add_share_snapshot_access
Revision ID: a77e2ad5012d
Revises: e1949a93157a
Create Date: 2016-07-15 13:32:19.417771
"""
# revision identifiers, used by Alembic.
revision = 'a77e2ad5012d'
down_revision = 'e1949a93157a'
from manila.common import constants
from manila.db.migrations import utils
from alembic import op
import sqlalchemy as sa
def upgrade():
op.create_table(
'share_snapshot_access_map',
sa.Column('id', sa.String(36), primary_key=True),
sa.Column('created_at', sa.DateTime),
sa.Column('updated_at', sa.DateTime),
sa.Column('deleted_at', sa.DateTime),
sa.Column('deleted', sa.String(36), default='False'),
sa.Column('share_snapshot_id', sa.String(36),
sa.ForeignKey('share_snapshots.id',
name='ssam_snapshot_fk')),
sa.Column('access_type', sa.String(255)),
sa.Column('access_to', sa.String(255))
)
op.create_table(
'share_snapshot_instance_access_map',
sa.Column('id', sa.String(36), primary_key=True),
sa.Column('created_at', sa.DateTime),
sa.Column('updated_at', sa.DateTime),
sa.Column('deleted_at', sa.DateTime),
sa.Column('deleted', sa.String(36), default='False'),
sa.Column('share_snapshot_instance_id', sa.String(36),
sa.ForeignKey('share_snapshot_instances.id',
name='ssiam_snapshot_instance_fk')),
sa.Column('access_id', sa.String(36),
sa.ForeignKey('share_snapshot_access_map.id',
name='ssam_access_fk')),
sa.Column('state', sa.String(255),
default=constants.ACCESS_STATE_QUEUED_TO_APPLY)
)
op.create_table(
'share_snapshot_instance_export_locations',
sa.Column('id', sa.String(36), primary_key=True),
sa.Column('created_at', sa.DateTime),
sa.Column('updated_at', sa.DateTime),
sa.Column('deleted_at', sa.DateTime),
sa.Column('deleted', sa.String(36), default='False'),
sa.Column('share_snapshot_instance_id', sa.String(36),
sa.ForeignKey('share_snapshot_instances.id',
name='ssiel_snapshot_instance_fk')),
sa.Column('path', sa.String(2000)),
sa.Column('is_admin_only', sa.Boolean, default=False, nullable=False)
)
op.add_column('shares',
sa.Column('mount_snapshot_support', sa.Boolean,
default=False))
connection = op.get_bind()
shares_table = utils.load_table('shares', connection)
op.execute(
shares_table.update().where(
shares_table.c.deleted == 'False').values({
'mount_snapshot_support': False,
})
)
def downgrade():
op.drop_table('share_snapshot_instance_export_locations')
op.drop_table('share_snapshot_instance_access_map')
op.drop_table('share_snapshot_access_map')
op.drop_column('shares', 'mount_snapshot_support')

View File

@ -1850,6 +1850,18 @@ def _set_instances_share_access_data(context, instance_accesses, session):
return instance_accesses
def _set_instances_snapshot_access_data(context, instance_accesses, session):
if instance_accesses and not isinstance(instance_accesses, list):
instance_accesses = [instance_accesses]
for instance_access in instance_accesses:
snapshot_access = share_snapshot_access_get(
context, instance_access['access_id'], session=session)
instance_access.set_snapshot_access_data(snapshot_access)
return instance_accesses
@require_context
def share_access_get_all_by_type_and_access(context, share_id, access_type,
access):
@ -1960,8 +1972,19 @@ def share_snapshot_instance_delete(context, snapshot_instance_id,
session = session or get_session()
with session.begin():
snapshot_instance_ref = share_snapshot_instance_get(
context, snapshot_instance_id, session=session)
access_rules = share_snapshot_access_get_all_for_snapshot_instance(
context, snapshot_instance_id, session=session)
for rule in access_rules:
share_snapshot_instance_access_delete(
context, rule['access_id'], snapshot_instance_id)
for el in snapshot_instance_ref.export_locations:
share_snapshot_instance_export_location_delete(context, el['id'])
snapshot_instance_ref.soft_delete(
session=session, update_status=True)
snapshot = share_snapshot_get(
@ -2233,6 +2256,269 @@ def share_snapshot_update(context, snapshot_id, values):
return snapshot_ref
#################################
@require_context
def share_snapshot_access_create(context, values):
values = ensure_model_dict_has_id(values)
session = get_session()
with session.begin():
access_ref = models.ShareSnapshotAccessMapping()
access_ref.update(values)
access_ref.save(session=session)
snapshot = share_snapshot_get(context, values['share_snapshot_id'],
session=session)
for instance in snapshot.instances:
vals = {
'share_snapshot_instance_id': instance['id'],
'access_id': access_ref['id'],
}
_share_snapshot_instance_access_create(vals, session)
return share_snapshot_access_get(context, access_ref['id'])
def _share_snapshot_access_get_query(context, session, filters,
read_deleted='no'):
query = model_query(context, models.ShareSnapshotAccessMapping,
session=session, read_deleted=read_deleted)
return query.filter_by(**filters)
def _share_snapshot_instance_access_get_query(context, session,
access_id=None,
share_snapshot_instance_id=None):
filters = {'deleted': 'False'}
if access_id is not None:
filters.update({'access_id': access_id})
if share_snapshot_instance_id is not None:
filters.update(
{'share_snapshot_instance_id': share_snapshot_instance_id})
return model_query(context, models.ShareSnapshotInstanceAccessMapping,
session=session).filter_by(**filters)
@require_context
def share_snapshot_instance_access_get_all(context, access_id, session):
rules = _share_snapshot_instance_access_get_query(
context, session, access_id=access_id).all()
return rules
@require_context
def share_snapshot_access_get(context, access_id, session=None):
session = session or get_session()
access = _share_snapshot_access_get_query(
context, session, {'id': access_id}).first()
if access:
return access
else:
raise exception.NotFound()
def _share_snapshot_instance_access_create(values, session):
access_ref = models.ShareSnapshotInstanceAccessMapping()
access_ref.update(ensure_model_dict_has_id(values))
access_ref.save(session=session)
return access_ref
@require_context
def share_snapshot_access_get_all_for_share_snapshot(context,
share_snapshot_id,
filters):
session = get_session()
filters['share_snapshot_id'] = share_snapshot_id
access_list = _share_snapshot_access_get_query(
context, session, filters).all()
return access_list
@require_context
def share_snapshot_access_get_all_for_snapshot_instance(
context, snapshot_instance_id, filters=None,
with_snapshot_access_data=True, session=None):
"""Get all access rules related to a certain snapshot instance."""
session = session or get_session()
filters = copy.deepcopy(filters) if filters else {}
filters.update({'share_snapshot_instance_id': snapshot_instance_id})
query = _share_snapshot_instance_access_get_query(context, session)
legal_filter_keys = (
'id', 'share_snapshot_instance_id', 'access_id', 'state')
query = exact_filter(
query, models.ShareSnapshotInstanceAccessMapping, filters,
legal_filter_keys)
instance_accesses = query.all()
if with_snapshot_access_data:
instance_accesses = _set_instances_snapshot_access_data(
context, instance_accesses, session)
return instance_accesses
@require_context
def share_snapshot_instance_access_update(
context, access_id, instance_id, updates):
snapshot_access_fields = ('access_type', 'access_to')
snapshot_access_map_updates, share_instance_access_map_updates = (
_extract_subdict_by_fields(updates, snapshot_access_fields)
)
session = get_session()
with session.begin():
snapshot_access = _share_snapshot_access_get_query(
context, session, {'id': access_id}).first()
if not snapshot_access:
raise exception.NotFound()
snapshot_access.update(snapshot_access_map_updates)
snapshot_access.save(session=session)
access = _share_snapshot_instance_access_get_query(
context, session, access_id=access_id,
share_snapshot_instance_id=instance_id).first()
if not access:
raise exception.NotFound()
access.update(share_instance_access_map_updates)
access.save(session=session)
return access
@require_context
def share_snapshot_instance_access_get(
context, access_id, share_snapshot_instance_id,
with_snapshot_access_data=True):
session = get_session()
with session.begin():
access = _share_snapshot_instance_access_get_query(
context, session, access_id=access_id,
share_snapshot_instance_id=share_snapshot_instance_id).first()
if access is None:
raise exception.NotFound()
if with_snapshot_access_data:
return _set_instances_snapshot_access_data(
context, access, session)[0]
else:
return access
@require_context
def share_snapshot_instance_access_delete(
context, access_id, snapshot_instance_id):
session = get_session()
with session.begin():
rule = _share_snapshot_instance_access_get_query(
context, session, access_id=access_id,
share_snapshot_instance_id=snapshot_instance_id).first()
if not rule:
exception.NotFound()
rule.soft_delete(session, update_status=True,
status_field_name='state')
other_mappings = share_snapshot_instance_access_get_all(
context, rule['access_id'], session)
if len(other_mappings) == 0:
(
session.query(models.ShareSnapshotAccessMapping)
.filter_by(id=rule['access_id'])
.soft_delete(update_status=True, status_field_name='state')
)
@require_context
def share_snapshot_instance_export_location_create(context, values):
values = ensure_model_dict_has_id(values)
session = get_session()
with session.begin():
access_ref = models.ShareSnapshotInstanceExportLocation()
access_ref.update(values)
access_ref.save(session=session)
return access_ref
def _share_snapshot_instance_export_locations_get_query(context, session,
values):
query = model_query(context, models.ShareSnapshotInstanceExportLocation,
session=session)
return query.filter_by(**values)
@require_context
def share_snapshot_export_locations_get(context, snapshot_id):
session = get_session()
snapshot = share_snapshot_get(context, snapshot_id, session=session)
ins_ids = [ins['id'] for ins in snapshot.instances]
export_locations = _share_snapshot_instance_export_locations_get_query(
context, session, {}).filter(
models.ShareSnapshotInstanceExportLocation.
share_snapshot_instance_id.in_(ins_ids)).all()
return export_locations
@require_context
def share_snapshot_instance_export_locations_get_all(
context, share_snapshot_instance_id):
session = get_session()
export_locations = _share_snapshot_instance_export_locations_get_query(
context, session,
{'share_snapshot_instance_id': share_snapshot_instance_id}).all()
return export_locations
@require_context
def share_snapshot_instance_export_location_get(context, el_id):
session = get_session()
export_location = _share_snapshot_instance_export_locations_get_query(
context, session, {'id': el_id}).first()
if export_location:
return export_location
else:
raise exception.NotFound()
@require_context
def share_snapshot_instance_export_location_delete(context, el_id):
session = get_session()
with session.begin():
el = _share_snapshot_instance_export_locations_get_query(
context, session, {'id': el_id}).first()
if not el:
exception.NotFound()
el.soft_delete(session=session)
#################################

View File

@ -306,6 +306,7 @@ class Share(BASE, ManilaBase):
create_share_from_snapshot_support = Column(Boolean, default=True)
revert_to_snapshot_support = Column(Boolean, default=False)
replication_type = Column(String(255), nullable=True)
mount_snapshot_support = Column(Boolean, default=False)
share_proto = Column(String(255))
is_public = Column(Boolean, default=False)
share_group_id = Column(String(36),
@ -550,20 +551,7 @@ class ShareAccessMapping(BASE, ManilaBase):
An access rule is supposed to be truly 'active' when it has been
applied across all of the share instances of the parent share object.
"""
state = None
if len(self.instance_mappings) > 0:
order = (constants.ACCESS_STATE_ERROR,
constants.ACCESS_STATE_DENYING,
constants.ACCESS_STATE_QUEUED_TO_DENY,
constants.ACCESS_STATE_QUEUED_TO_APPLY,
constants.ACCESS_STATE_APPLYING,
constants.ACCESS_STATE_ACTIVE)
sorted_instance_mappings = sorted(
self.instance_mappings, key=lambda x: order.index(x['state']))
state = sorted_instance_mappings[0].state
return state
return get_aggregated_access_rules_state(self.instance_mappings)
instance_mappings = orm.relationship(
"ShareInstanceAccessMapping",
@ -620,6 +608,25 @@ class ShareSnapshot(BASE, ManilaBase):
raise AttributeError(item)
@property
def export_locations(self):
# TODO(gouthamr): Return AZ specific export locations for replicated
# snapshots.
# NOTE(gouthamr): For a replicated snapshot, export locations of the
# 'active' instances are chosen, if 'available'.
all_export_locations = []
select_instances = list(filter(
lambda x: (x['share_instance']['replica_state'] ==
constants.REPLICA_STATE_ACTIVE),
self.instances)) or self.instances
for instance in select_instances:
if instance['status'] == constants.STATUS_AVAILABLE:
for export_location in instance.export_locations:
all_export_locations.append(export_location)
return all_export_locations
@property
def name(self):
return CONF.share_snapshot_name_template % self.id
@ -748,6 +755,92 @@ class ShareSnapshotInstance(BASE, ManilaBase):
'ShareSnapshotInstance.deleted == "False")')
)
export_locations = orm.relationship(
"ShareSnapshotInstanceExportLocation",
lazy='immediate',
primaryjoin=(
'and_('
'ShareSnapshotInstance.id == '
'ShareSnapshotInstanceExportLocation.share_snapshot_instance_id, '
'ShareSnapshotInstanceExportLocation.deleted == "False")'
)
)
class ShareSnapshotAccessMapping(BASE, ManilaBase):
"""Represents access to share snapshot."""
__tablename__ = 'share_snapshot_access_map'
@property
def state(self):
"""Get the aggregated 'state' from all the instance mapping states.
An access rule is supposed to be truly 'active' when it has been
applied across all of the share snapshot instances of the parent
share snapshot object.
"""
return get_aggregated_access_rules_state(self.instance_mappings)
id = Column(String(36), primary_key=True)
deleted = Column(String(36), default='False')
share_snapshot_id = Column(String(36), ForeignKey('share_snapshots.id'))
access_type = Column(String(255))
access_to = Column(String(255))
instance_mappings = orm.relationship(
"ShareSnapshotInstanceAccessMapping",
lazy='immediate',
primaryjoin=(
'and_('
'ShareSnapshotAccessMapping.id == '
'ShareSnapshotInstanceAccessMapping.access_id, '
'ShareSnapshotInstanceAccessMapping.deleted == "False")'
)
)
class ShareSnapshotInstanceAccessMapping(BASE, ManilaBase):
"""Represents access to individual share snapshot instances."""
__tablename__ = 'share_snapshot_instance_access_map'
_proxified_properties = ('share_snapshot_id', 'access_type', 'access_to')
def set_snapshot_access_data(self, snapshot_access):
for snapshot_access_attr in self._proxified_properties:
setattr(self, snapshot_access_attr,
snapshot_access[snapshot_access_attr])
id = Column(String(36), primary_key=True)
deleted = Column(String(36), default='False')
share_snapshot_instance_id = Column(String(36), ForeignKey(
'share_snapshot_instances.id'))
access_id = Column(String(36), ForeignKey('share_snapshot_access_map.id'))
state = Column(Enum(*constants.ACCESS_RULES_STATES),
default=constants.ACCESS_STATE_QUEUED_TO_APPLY)
instance = orm.relationship(
"ShareSnapshotInstance",
lazy='immediate',
primaryjoin=(
'and_('
'ShareSnapshotInstanceAccessMapping.share_snapshot_instance_id == '
'ShareSnapshotInstance.id, '
'ShareSnapshotInstanceAccessMapping.deleted == "False")'
)
)
class ShareSnapshotInstanceExportLocation(BASE, ManilaBase):
"""Represents export locations of share snapshot instances."""
__tablename__ = 'share_snapshot_instance_export_locations'
id = Column(String(36), primary_key=True)
share_snapshot_instance_id = Column(
String(36), ForeignKey('share_snapshot_instances.id'), nullable=False)
path = Column(String(2000))
is_admin_only = Column(Boolean, default=False, nullable=False)
deleted = Column(String(36), default='False')
class SecurityService(BASE, ManilaBase):
"""Security service information for manila shares."""
@ -1117,3 +1210,20 @@ def get_access_rules_status(instances):
break
return share_access_status
def get_aggregated_access_rules_state(instance_mappings):
state = None
if len(instance_mappings) > 0:
order = (constants.ACCESS_STATE_ERROR,
constants.ACCESS_STATE_DENYING,
constants.ACCESS_STATE_QUEUED_TO_DENY,
constants.ACCESS_STATE_QUEUED_TO_APPLY,
constants.ACCESS_STATE_APPLYING,
constants.ACCESS_STATE_ACTIVE)
sorted_instance_mappings = sorted(
instance_mappings, key=lambda x: order.index(x['state']))
state = sorted_instance_mappings[0].state
return state

View File

@ -446,6 +446,10 @@ class ShareAccessExists(ManilaException):
message = _("Share access %(access_type)s:%(access)s exists.")
class ShareSnapshotAccessExists(InvalidInput):
message = _("Share snapshot access %(access_type)s:%(access)s exists.")
class InvalidShareAccess(Invalid):
message = _("Invalid access rule: %(reason)s")
@ -491,6 +495,10 @@ class InvalidShareSnapshot(Invalid):
message = _("Invalid share snapshot: %(reason)s.")
class InvalidShareSnapshotInstance(Invalid):
message = _("Invalid share snapshot instance: %(reason)s.")
class ManageInvalidShareSnapshot(InvalidShareSnapshot):
message = _("Manage existing share snapshot failed due to "
"invalid share snapshot: %(reason)s.")

View File

@ -131,6 +131,7 @@ class HostState(object):
self.snapshot_support = True
self.create_share_from_snapshot_support = True
self.revert_to_snapshot_support = False
self.mount_snapshot_support = False
self.dedupe = False
self.compression = False
self.replication_type = None
@ -302,6 +303,9 @@ class HostState(object):
pool_cap['revert_to_snapshot_support'] = (
self.revert_to_snapshot_support)
if 'mount_snapshot_support' not in pool_cap:
pool_cap['mount_snapshot_support'] = self.mount_snapshot_support
if 'dedupe' not in pool_cap:
pool_cap['dedupe'] = self.dedupe
@ -326,6 +330,8 @@ class HostState(object):
'create_share_from_snapshot_support')
self.revert_to_snapshot_support = capability.get(
'revert_to_snapshot_support', False)
self.mount_snapshot_support = capability.get(
'mount_snapshot_support', False)
self.updated = capability['timestamp']
self.replication_type = capability.get('replication_type')
self.replication_domain = capability.get('replication_domain')

View File

@ -46,6 +46,7 @@ def generate_stats(host_state, properties):
'create_share_from_snapshot_support':
host_state.create_share_from_snapshot_support,
'revert_to_snapshot_support': host_state.revert_to_snapshot_support,
'mount_snapshot_support': host_state.mount_snapshot_support,
'replication_domain': host_state.replication_domain,
'replication_type': host_state.replication_type,
'provisioned_capacity_gb': host_state.provisioned_capacity_gb,

View File

@ -275,12 +275,16 @@ class API(base.Base):
constants.ExtraSpecs.CREATE_SHARE_FROM_SNAPSHOT_SUPPORT)
revert_to_snapshot_key = (
constants.ExtraSpecs.REVERT_TO_SNAPSHOT_SUPPORT)
mount_snapshot_support_key = (
constants.ExtraSpecs.MOUNT_SNAPSHOT_SUPPORT)
snapshot_support_default = inferred_map.get(snapshot_support_key)
create_share_from_snapshot_support_default = inferred_map.get(
create_share_from_snapshot_key)
revert_to_snapshot_support_default = inferred_map.get(
revert_to_snapshot_key)
mount_snapshot_support_default = inferred_map.get(
constants.ExtraSpecs.MOUNT_SNAPSHOT_SUPPORT)
if share_type:
snapshot_support = share_types.parse_boolean_extra_spec(
@ -299,6 +303,11 @@ class API(base.Base):
share_type.get('extra_specs', {}).get(
revert_to_snapshot_key,
revert_to_snapshot_support_default)))
mount_snapshot_support = share_types.parse_boolean_extra_spec(
mount_snapshot_support_key, share_type.get(
'extra_specs', {}).get(
mount_snapshot_support_key,
mount_snapshot_support_default))
replication_type = share_type.get('extra_specs', {}).get(
'replication_type')
else:
@ -306,6 +315,7 @@ class API(base.Base):
create_share_from_snapshot_support = (
create_share_from_snapshot_support_default)
revert_to_snapshot_support = revert_to_snapshot_support_default
mount_snapshot_support = mount_snapshot_support_default
replication_type = None
return {
@ -314,6 +324,7 @@ class API(base.Base):
create_share_from_snapshot_support,
'revert_to_snapshot_support': revert_to_snapshot_support,
'replication_type': replication_type,
'mount_snapshot_support': mount_snapshot_support,
}
def create_instance(self, context, share, share_network_id=None,
@ -399,6 +410,7 @@ class API(base.Base):
'create_share_from_snapshot_support':
share['create_share_from_snapshot_support'],
'revert_to_snapshot_support': share['revert_to_snapshot_support'],
'mount_snapshot_support': share['mount_snapshot_support'],
'share_proto': share['share_proto'],
'share_type_id': share_type_id,
'is_public': share['is_public'],
@ -646,7 +658,11 @@ class API(base.Base):
share_type.get('extra_specs', {}).get(
'revert_to_snapshot_support')
),
'mount_snapshot_support': kwargs.get(
'mount_snapshot_support',
share_type.get('extra_specs', {}).get(
'mount_snapshot_support')
),
'share_proto': kwargs.get('share_proto', share.get('share_proto')),
'share_type_id': share_type['id'],
'is_public': kwargs.get('is_public', share.get('is_public')),
@ -1819,3 +1835,77 @@ class API(base.Base):
LOG.info(_LI("Shrink share (id=%(id)s) request issued successfully."
" New size: %(size)s") % {'id': share['id'],
'size': new_size})
def snapshot_allow_access(self, context, snapshot, access_type, access_to):
"""Allow access to a share snapshot."""
filters = {'access_to': access_to,
'access_type': access_type}
access_list = self.db.share_snapshot_access_get_all_for_share_snapshot(
context, snapshot['id'], filters)
if len(access_list) > 0:
raise exception.ShareSnapshotAccessExists(access_type=access_type,
access=access_to)
values = {
'share_snapshot_id': snapshot['id'],
'access_type': access_type,
'access_to': access_to,
}
if any((instance['status'] != constants.STATUS_AVAILABLE) or
(instance['share_instance']['host'] is None)
for instance in snapshot.instances):
msg = _("New access rules cannot be applied while the snapshot or "
"any of its replicas or migration copies lacks a valid "
"host or is not in %s state.") % constants.STATUS_AVAILABLE
raise exception.InvalidShareSnapshotInstance(reason=msg)
access = self.db.share_snapshot_access_create(context, values)
for snapshot_instance in snapshot.instances:
self.share_rpcapi.snapshot_update_access(
context, snapshot_instance)
return access
def snapshot_deny_access(self, context, snapshot, access):
"""Deny access to a share snapshot."""
if any((instance['status'] != constants.STATUS_AVAILABLE) or
(instance['share_instance']['host'] is None)
for instance in snapshot.instances):
msg = _("Access rules cannot be denied while the snapshot or "
"any of its replicas or migration copies lacks a valid "
"host or is not in %s state.") % constants.STATUS_AVAILABLE
raise exception.InvalidShareSnapshotInstance(reason=msg)
for snapshot_instance in snapshot.instances:
rule = self.db.share_snapshot_instance_access_get(
context, access['id'], snapshot_instance['id'])
self.db.share_snapshot_instance_access_update(
context, rule['access_id'], snapshot_instance['id'],
{'state': constants.ACCESS_STATE_QUEUED_TO_DENY})
self.share_rpcapi.snapshot_update_access(
context, snapshot_instance)
def snapshot_access_get_all(self, context, snapshot):
"""Returns all access rules for share snapshot."""
rules = self.db.share_snapshot_access_get_all_for_share_snapshot(
context, snapshot['id'], {})
return rules
def snapshot_access_get(self, context, access_id):
"""Returns snapshot access rule with the id."""
rule = self.db.share_snapshot_access_get(context, access_id)
return rule
def snapshot_export_locations_get(self, context, snapshot):
return self.db.share_snapshot_export_locations_get(context, snapshot)
def snapshot_export_location_get(self, context, el_id):
return self.db.share_snapshot_instance_export_location_get(context,
el_id)

View File

@ -622,6 +622,8 @@ class ShareDriver(object):
:param snapshot: Snapshot model. Share model could be
retrieved through snapshot['share'].
:param share_server: Share server model or None.
:return: None or a dictionary with key 'export_locations' containing
a list of export locations, if snapshots can be mounted.
"""
raise NotImplementedError()
@ -935,7 +937,9 @@ class ShareDriver(object):
}
:return: model_update dictionary with required key 'size',
which should contain size of the share snapshot.
which should contain size of the share snapshot, and key
'export_locations' containing a list of export locations, if
snapshots can be mounted.
"""
raise NotImplementedError()
@ -1076,6 +1080,7 @@ class ShareDriver(object):
self.creating_shares_from_snapshots_is_supported),
revert_to_snapshot_support=False,
share_group_snapshot_support=self.snapshots_are_supported,
mount_snapshot_support=False,
replication_domain=self.replication_domain,
filter_function=self.get_filter_function(),
goodness_function=self.get_goodness_function(),
@ -2325,3 +2330,35 @@ class ShareDriver(object):
:return: None
"""
return None
def snapshot_update_access(self, context, snapshot, access_rules,
add_rules, delete_rules, share_server=None):
"""Update access rules for given snapshot.
``access_rules`` contains all access_rules that need to be on the
share. If the driver can make bulk access rule updates, it can
safely ignore the ``add_rules`` and ``delete_rules`` parameters.
If the driver cannot make bulk access rule changes, it can rely on
new rules to be present in ``add_rules`` and rules that need to be
removed to be present in ``delete_rules``.
When a rule in ``add_rules`` already exists in the back end, drivers
must not raise an exception. When a rule in ``delete_rules`` was never
applied, drivers must not raise an exception, or attempt to set the
rule to ``error`` state.
``add_rules`` and ``delete_rules`` can be empty lists, in this
situation, drivers should ensure that the rules present in
``access_rules`` are the same as those on the back end.
:param context: Current context
:param snapshot: Snapshot model with snapshot data.
:param access_rules: All access rules for given snapshot
:param add_rules: Empty List or List of access rules which should be
added. access_rules already contains these rules.
:param delete_rules: Empty List or List of access rules which should be
removed. access_rules doesn't contain these rules.
:param share_server: None or Share server model
"""
raise NotImplementedError()

View File

@ -191,6 +191,7 @@ class LVMShareDriver(LVMMixin, driver.ShareDriver):
'snapshot_support': True,
'create_share_from_snapshot_support': True,
'revert_to_snapshot_support': True,
'mount_snapshot_support': True,
'driver_name': 'LVMShareDriver',
'pools': self.get_share_server_pools()
}
@ -361,6 +362,7 @@ class LVMShareDriver(LVMMixin, driver.ShareDriver):
self._execute('resize2fs', device_name, run_as_root=True)
def revert_to_snapshot(self, context, snapshot, share_server=None):
self._remove_export(context, snapshot)
# First we merge the snapshot LV and the share LV
# This won't actually do anything until the LV is reactivated
snap_lv_name = "%s/%s" % (self.configuration.lvm_share_volume_group,
@ -381,3 +383,72 @@ class LVMShareDriver(LVMMixin, driver.ShareDriver):
# Finally we can mount the share again
device_name = self._get_local_path(share)
self._mount_device(share, device_name)
device_name = self._get_local_path(snapshot)
self._mount_device(snapshot, device_name)
def create_snapshot(self, context, snapshot, share_server=None):
self._create_snapshot(context, snapshot)
helper = self._get_helper(snapshot['share'])
exports = helper.create_exports(self.share_server, snapshot['name'])
device_name = self._get_local_path(snapshot)
self._mount_device(snapshot, device_name)
return {'export_locations': exports}
def delete_snapshot(self, context, snapshot, share_server=None):
self._remove_export(context, snapshot)
super(LVMShareDriver, self).delete_snapshot(context, snapshot,
share_server)
def snapshot_update_access(self, context, snapshot, access_rules,
add_rules, delete_rules, share_server=None):
"""Update access rules for given snapshot.
This driver has two different behaviors according to parameters:
1. Recovery after error - 'access_rules' contains all access_rules,
'add_rules' and 'delete_rules' shall be empty. Previously existing
access rules are cleared and then added back according
to 'access_rules'.
2. Adding/Deleting of several access rules - 'access_rules' contains
all access_rules, 'add_rules' and 'delete_rules' contain rules which
should be added/deleted. Rules in 'access_rules' are ignored and
only rules from 'add_rules' and 'delete_rules' are applied.
:param context: Current context
:param snapshot: Snapshot model with snapshot data.
:param access_rules: All access rules for given snapshot
:param add_rules: Empty List or List of access rules which should be
added. access_rules already contains these rules.
:param delete_rules: Empty List or List of access rules which should be
removed. access_rules doesn't contain these rules.
:param share_server: None or Share server model
"""
helper = self._get_helper(snapshot['share'])
access_rules, add_rules, delete_rules = change_rules_to_readonly(
access_rules, add_rules, delete_rules)
helper.update_access(self.share_server,
snapshot['name'], access_rules,
add_rules=add_rules, delete_rules=delete_rules)
def change_rules_to_readonly(access_rules, add_rules, delete_rules):
dict_access_rules = cast_access_object_to_dict_in_readonly(access_rules)
dict_add_rules = cast_access_object_to_dict_in_readonly(add_rules)
dict_delete_rules = cast_access_object_to_dict_in_readonly(delete_rules)
return dict_access_rules, dict_add_rules, dict_delete_rules
def cast_access_object_to_dict_in_readonly(rules):
dict_rules = []
for rule in rules:
dict_rules.append({
'access_level': 'ro',
'access_type': rule['access_type'],
'access_to': rule['access_to']
})
return dict_rules

View File

@ -48,6 +48,7 @@ from manila.share import drivers_private_data
from manila.share import migration
from manila.share import rpcapi as share_rpcapi
from manila.share import share_types
from manila.share import snapshot_access
from manila.share import utils as share_utils
from manila import utils
@ -189,7 +190,7 @@ def add_hooks(f):
class ShareManager(manager.SchedulerDependentManager):
"""Manages NAS storages."""
RPC_API_VERSION = '1.16'
RPC_API_VERSION = '1.17'
def __init__(self, share_driver=None, service_name=None, *args, **kwargs):
"""Load the driver from args, or from flags."""
@ -219,6 +220,8 @@ class ShareManager(manager.SchedulerDependentManager):
)
self.access_helper = access.ShareInstanceAccess(self.db, self.driver)
self.snapshot_access_helper = (
snapshot_access.ShareSnapshotInstanceAccess(self.db, self.driver))
self.migration_wait_access_rules_timeout = (
CONF.migration_wait_access_rules_timeout)
@ -343,6 +346,34 @@ class ShareManager(manager.SchedulerDependentManager):
{'s_id': share_instance['id']},
)
snapshot_instances = (
self.db.share_snapshot_instance_get_all_with_filters(
ctxt, {'share_instance_ids': share_instance['id']},
with_share_data=True))
for snap_instance in snapshot_instances:
rules = (
self.db.
share_snapshot_access_get_all_for_snapshot_instance(
ctxt, snap_instance['id']))
# NOTE(ganso): We don't invoke update_access for snapshots if
# we don't have invalid rules or pending updates
if any(r['state'] in (constants.ACCESS_STATE_DENYING,
constants.ACCESS_STATE_QUEUED_TO_DENY,
constants.ACCESS_STATE_APPLYING,
constants.ACCESS_STATE_QUEUED_TO_APPLY)
for r in rules):
try:
self.snapshot_access_helper.update_access_rules(
ctxt, snap_instance['id'], share_server)
except Exception:
LOG.exception(_LE(
"Unexpected error occurred while updating "
"access rules for snapshot instance %s."),
snap_instance['id'])
self.publish_service_capabilities(ctxt)
LOG.info(_LI("Finished initialization of driver: '%(driver)s"
"@%(host)s'"),
@ -2233,6 +2264,18 @@ class ShareManager(manager.SchedulerDependentManager):
"snapshot_gigabytes": snapshot_update['size'],
})
snapshot_export_locations = snapshot_update.pop(
'export_locations', [])
for el in snapshot_export_locations:
values = {
'share_snapshot_instance_id': snapshot_instance['id'],
'path': el['path'],
'is_admin_only': el['is_admin_only'],
}
self.db.share_snapshot_instance_export_location_create(context,
values)
snapshot_update.update({
'status': constants.STATUS_AVAILABLE,
'progress': '100%',
@ -2355,6 +2398,20 @@ class ShareManager(manager.SchedulerDependentManager):
msg)
return
if self.configuration.safe_get('unmanage_remove_access_rules'):
try:
self.snapshot_access_helper.update_access_rules(
context,
snapshot_instance['id'],
delete_all_rules=True,
share_server=share_server)
except Exception:
LOG.exception(
_LE("Cannot remove access rules of snapshot %s."),
snapshot_id)
self.db.share_snapshot_update(context, snapshot_id, status)
return
try:
self.driver.unmanage_snapshot(snapshot_instance)
except exception.UnmanageInvalidShareSnapshot as e:
@ -2561,6 +2618,18 @@ class ShareManager(manager.SchedulerDependentManager):
snapshot_instance_id,
{'status': constants.STATUS_ERROR})
snapshot_export_locations = model_update.pop('export_locations', [])
for el in snapshot_export_locations:
values = {
'share_snapshot_instance_id': snapshot_instance_id,
'path': el['path'],
'is_admin_only': el['is_admin_only'],
}
self.db.share_snapshot_instance_export_location_create(context,
values)
if model_update.get('status') in (None, constants.STATUS_AVAILABLE):
model_update['status'] = constants.STATUS_AVAILABLE
model_update['progress'] = '100%'
@ -2589,6 +2658,21 @@ class ShareManager(manager.SchedulerDependentManager):
snapshot_instance = self._get_snapshot_instance_dict(
context, snapshot_instance)
share_ref = self.db.share_get(context, snapshot_ref['share_id'])
if share_ref['mount_snapshot_support']:
try:
self.snapshot_access_helper.update_access_rules(
context, snapshot_instance['id'], delete_all_rules=True,
share_server=share_server)
except Exception:
LOG.exception(
_LE("Failed to remove access rules for snapshot %s."),
snapshot_instance['id'])
LOG.warning(_LW("The driver was unable to remove access rules "
"for snapshot %s. Moving on."),
snapshot_instance['snapshot_id'])
try:
self.driver.delete_snapshot(context, snapshot_instance,
share_server=share_server)
@ -3635,3 +3719,13 @@ class ShareManager(manager.SchedulerDependentManager):
})
return snapshot_instance_ref
def snapshot_update_access(self, context, snapshot_instance_id):
snapshot_instance = self.db.share_snapshot_instance_get(
context, snapshot_instance_id, with_share_data=True)
share_server = self._get_share_server(
context, snapshot_instance['share_instance'])
self.snapshot_access_helper.update_access_rules(
context, snapshot_instance['id'], share_server=share_server)

View File

@ -73,6 +73,7 @@ class ShareAPI(object):
create_cgsnapshot, and delete_cgsnapshot methods to
create_share_group, delete_share_group
create_share_group_snapshot, and delete_share_group_snapshot
1.17 - Add snapshot_update_access()
"""
BASE_RPC_API_VERSION = '1.0'
@ -81,7 +82,7 @@ class ShareAPI(object):
super(ShareAPI, self).__init__()
target = messaging.Target(topic=CONF.share_topic,
version=self.BASE_RPC_API_VERSION)
self.client = rpc.get_client(target, version_cap='1.16')
self.client = rpc.get_client(target, version_cap='1.17')
def create_share_instance(self, context, share_instance, host,
request_spec, filter_properties,
@ -341,3 +342,10 @@ class ShareAPI(object):
call_context.cast(context,
'create_share_server',
share_server_id=share_server_id)
def snapshot_update_access(self, context, snapshot_instance):
host = utils.extract_host(snapshot_instance['share_instance']['host'])
call_context = self.client.prepare(server=host, version='1.17')
call_context.cast(context,
'snapshot_update_access',
snapshot_instance_id=snapshot_instance['id'])

View File

@ -270,6 +270,8 @@ def is_valid_optional_extra_spec(key, value):
return parse_boolean_extra_spec(key, value) is not None
elif key == constants.ExtraSpecs.REPLICATION_TYPE_SPEC:
return value in constants.ExtraSpecs.REPLICATION_TYPES
elif key == constants.ExtraSpecs.MOUNT_SNAPSHOT_SUPPORT:
return parse_boolean_extra_spec(key, value) is not None
return False

View File

@ -0,0 +1,167 @@
# Copyright (c) 2016 Hitachi Data Systems
# 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.
from oslo_log import log
from manila.common import constants
from manila.i18n import _LI
from manila import utils
LOG = log.getLogger(__name__)
class ShareSnapshotInstanceAccess(object):
def __init__(self, db, driver):
self.db = db
self.driver = driver
def update_access_rules(self, context, snapshot_instance_id,
delete_all_rules=False, share_server=None):
"""Update driver and database access rules for given snapshot instance.
:param context: current context
:param snapshot_instance_id: Id of the snapshot instance model
:param delete_all_rules: Whether all rules should be deleted.
:param share_server: Share server model or None
"""
snapshot_instance = self.db.share_snapshot_instance_get(
context, snapshot_instance_id, with_share_data=True)
snapshot_id = snapshot_instance['snapshot_id']
@utils.synchronized(
"update_access_rules_for_snapshot_%s" % snapshot_id, external=True)
def _update_access_rules_locked(*args, **kwargs):
return self._update_access_rules(*args, **kwargs)
_update_access_rules_locked(
context=context,
snapshot_instance=snapshot_instance,
delete_all_rules=delete_all_rules,
share_server=share_server,
)
def _update_access_rules(self, context, snapshot_instance,
delete_all_rules=None, share_server=None):
# NOTE(ganso): First let's get all the rules and the mappings.
rules = self.db.share_snapshot_access_get_all_for_snapshot_instance(
context, snapshot_instance['id'])
add_rules = []
delete_rules = []
if delete_all_rules:
# NOTE(ganso): We want to delete all rules.
delete_rules = rules
rules_to_be_on_snapshot = []
# NOTE(ganso): We select all deletable mappings.
for rule in rules:
# NOTE(ganso): No need to update the state if already set.
if rule['state'] != constants.ACCESS_STATE_DENYING:
self.db.share_snapshot_instance_access_update(
context, rule['access_id'], snapshot_instance['id'],
{'state': constants.ACCESS_STATE_DENYING})
else:
# NOTE(ganso): error'ed rules are to be left alone until
# reset back to "queued_to_deny" by API. Some drivers may
# attempt to reapply these rules, and later get deleted when
# requested.
rules_to_be_on_snapshot = [
r for r in rules if r['state'] not in (
constants.ACCESS_STATE_QUEUED_TO_DENY,
# NOTE(ganso): We select denying rules as a recovery
# mechanism for invalid rules during a restart.
constants.ACCESS_STATE_DENYING)
]
# NOTE(ganso): Process queued rules
for rule in rules:
# NOTE(ganso): We are barely handling recovery, so if any rule
# exists in 'applying' or 'denying' state, we add them again.
if rule['state'] in (constants.ACCESS_STATE_QUEUED_TO_APPLY,
constants.ACCESS_STATE_APPLYING):
if rule['state'] == (
constants.ACCESS_STATE_QUEUED_TO_APPLY):
self.db.share_snapshot_instance_access_update(
context, rule['access_id'],
snapshot_instance['id'],
{'state': constants.ACCESS_STATE_APPLYING})
add_rules.append(rule)
elif rule['state'] in (
constants.ACCESS_STATE_QUEUED_TO_DENY,
constants.ACCESS_STATE_DENYING):
if rule['state'] == (
constants.ACCESS_STATE_QUEUED_TO_DENY):
self.db.share_snapshot_instance_access_update(
context, rule['access_id'],
snapshot_instance['id'],
{'state': constants.ACCESS_STATE_DENYING})
delete_rules.append(rule)
try:
self.driver.snapshot_update_access(
context,
snapshot_instance,
rules_to_be_on_snapshot,
add_rules=add_rules,
delete_rules=delete_rules,
share_server=share_server)
# NOTE(ganso): successfully added rules transition to "active".
for rule in add_rules:
self.db.share_snapshot_instance_access_update(
context, rule['access_id'], snapshot_instance['id'],
{'state': constants.STATUS_ACTIVE})
except Exception:
# NOTE(ganso): if we failed, we set all the transitional rules
# to ERROR.
for rule in add_rules + delete_rules:
self.db.share_snapshot_instance_access_update(
context, rule['access_id'], snapshot_instance['id'],
{'state': constants.STATUS_ERROR})
raise
self._remove_access_rules(
context, delete_rules, snapshot_instance['id'])
if self._check_needs_refresh(context, snapshot_instance['id']):
self._update_access_rules(context, snapshot_instance,
share_server=share_server)
else:
LOG.info(_LI("Access rules were successfully applied for "
"snapshot instance: %s"), snapshot_instance['id'])
def _check_needs_refresh(self, context, snapshot_instance_id):
rules = self.db.share_snapshot_access_get_all_for_snapshot_instance(
context, snapshot_instance_id)
return (any(rule['state'] in (
constants.ACCESS_STATE_QUEUED_TO_APPLY,
constants.ACCESS_STATE_QUEUED_TO_DENY)
for rule in rules))
def _remove_access_rules(self, context, rules, snapshot_instance_id):
if not rules:
return
for rule in rules:
self.db.share_snapshot_instance_access_delete(
context, rule['access_id'], snapshot_instance_id)

View File

@ -43,6 +43,7 @@ def stub_share(id, **kwargs):
'snapshot_support': True,
'create_share_from_snapshot_support': True,
'revert_to_snapshot_support': False,
'mount_snapshot_support': False,
'replication_type': None,
'has_replicas': False,
}

View File

@ -194,6 +194,7 @@ class PaginationParamsTest(test.TestCase):
common.get_pagination_params(req))
@ddt.ddt
class MiscFunctionsTest(test.TestCase):
def test_remove_major_version_from_href(self):
@ -244,6 +245,39 @@ class MiscFunctionsTest(test.TestCase):
common.remove_version_from_href,
fixture)
def test_validate_cephx_id_invalid_with_period(self):
self.assertRaises(webob.exc.HTTPBadRequest,
common.validate_cephx_id,
"client.manila")
def test_validate_cephx_id_invalid_with_non_ascii(self):
self.assertRaises(webob.exc.HTTPBadRequest,
common.validate_cephx_id,
u"bj\u00F6rn")
@ddt.data("alice", "alice_bob", "alice bob")
def test_validate_cephx_id_valid(self, test_id):
common.validate_cephx_id(test_id)
@ddt.data(['ip', '1.1.1.1', False], ['user', 'alice', False],
['cert', 'alice', False], ['cephx', 'alice', True],
['ip', '172.24.41.0/24', False],)
@ddt.unpack
def test_validate_access(self, access_type, access_to, ceph):
common.validate_access(access_type=access_type, access_to=access_to,
enable_ceph=ceph)
@ddt.data(['ip', 'alice', False], ['ip', '1.1.1.0/10/12', False],
['ip', '255.255.255.265', False], ['ip', '1.1.1.0/34', False],
['cert', '', False], ['cephx', 'client.alice', True],
['group', 'alice', True], ['cephx', 'alice', False],
['cephx', '', True], ['user', 'bob', False])
@ddt.unpack
def test_validate_access_exception(self, access_type, access_to, ceph):
self.assertRaises(webob.exc.HTTPBadRequest, common.validate_access,
access_type=access_type, access_to=access_to,
enable_ceph=ceph)
@ddt.ddt
class ViewBuilderTest(test.TestCase):

View File

@ -744,20 +744,6 @@ class ShareAPITest(test.TestCase):
common.remove_invalid_options(ctx, search_opts, allowed_opts)
self.assertEqual(expected_opts, search_opts)
def test_validate_cephx_id_invalid_with_period(self):
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller._validate_cephx_id,
"client.manila")
def test_validate_cephx_id_invalid_with_non_ascii(self):
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller._validate_cephx_id,
u"bj\u00F6rn")
@ddt.data("alice", "alice_bob", "alice bob")
def test_validate_cephx_id_valid(self, test_id):
self.controller._validate_cephx_id(test_id)
def _fake_access_get(self, ctxt, access_id):

View File

@ -0,0 +1,116 @@
# Copyright (c) 2016 Hitachi Data Systems
# 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 manila.api.v2 import share_snapshot_export_locations as export_locations
from manila.common import constants
from manila import context
from manila.db.sqlalchemy import api as db_api
from manila import exception
from manila import test
from manila.tests.api import fakes
from manila.tests import db_utils
@ddt.ddt
class ShareSnapshotExportLocationsAPITest(test.TestCase):
def _get_request(self, version="2.32", use_admin_context=True):
req = fakes.HTTPRequest.blank(
'/snapshots/%s/export-locations' % self.snapshot['id'],
version=version, use_admin_context=use_admin_context)
return req
def setUp(self):
super(ShareSnapshotExportLocationsAPITest, self).setUp()
self.controller = (
export_locations.ShareSnapshotExportLocationController())
self.share = db_utils.create_share()
self.snapshot = db_utils.create_snapshot(
status=constants.STATUS_AVAILABLE,
share_id=self.share['id'])
self.snapshot_instance = db_utils.create_snapshot_instance(
status=constants.STATUS_AVAILABLE,
share_instance_id=self.share['instance']['id'],
snapshot_id=self.snapshot['id'])
self.values = {
'share_snapshot_instance_id': self.snapshot_instance['id'],
'path': 'fake/user_path',
'is_admin_only': True,
}
self.exp_loc = db_api.share_snapshot_instance_export_location_create(
context.get_admin_context(), self.values)
self.req = self._get_request()
def test_index(self):
self.mock_object(
db_api, 'share_snapshot_instance_export_locations_get_all',
mock.Mock(return_value=[self.exp_loc]))
out = self.controller.index(self._get_request('2.32'),
self.snapshot['id'])
values = {
'share_snapshot_export_locations': [{
'share_snapshot_instance_id': self.snapshot_instance['id'],
'path': 'fake/user_path',
'is_admin_only': True,
'id': self.exp_loc['id'],
'links': [{
'href': 'http://localhost/v1/fake/'
'share_snapshot_export_locations/' +
self.exp_loc['id'],
'rel': 'self'
}, {
'href': 'http://localhost/fake/'
'share_snapshot_export_locations/' +
self.exp_loc['id'],
'rel': 'bookmark'
}],
}]
}
self.assertSubDictMatch(values, out)
def test_show(self):
out = self.controller.show(self._get_request('2.32'),
self.snapshot['id'], self.exp_loc['id'])
self.assertSubDictMatch(
{'share_snapshot_export_location': self.values}, out)
@ddt.data('1.0', '2.0', '2.5', '2.8', '2.31')
def test_list_with_unsupported_version(self, version):
self.assertRaises(
exception.VersionNotFoundForAPIMethod,
self.controller.index,
self._get_request(version),
self.snapshot_instance['id'],
)
@ddt.data('1.0', '2.0', '2.5', '2.8', '2.31')
def test_show_with_unsupported_version(self, version):
self.assertRaises(
exception.VersionNotFoundForAPIMethod,
self.controller.show,
self._get_request(version),
self.snapshot['id'],
self.exp_loc['id']
)

View File

@ -0,0 +1,113 @@
# Copyright (c) 2016 Hitachi Data Systems
# 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 manila.api.v2 import share_snapshot_instance_export_locations as exp_loc
from manila.common import constants
from manila import context
from manila.db.sqlalchemy import api as db_api
from manila import exception
from manila import test
from manila.tests.api import fakes
from manila.tests import db_utils
@ddt.ddt
class ShareSnapshotInstanceExportLocationsAPITest(test.TestCase):
def _get_request(self, version="2.32", use_admin_context=True):
req = fakes.HTTPRequest.blank(
'/snapshot-instances/%s/export-locations' %
self.snapshot_instance['id'],
version=version, use_admin_context=use_admin_context)
return req
def setUp(self):
super(ShareSnapshotInstanceExportLocationsAPITest, self).setUp()
self.controller = (
exp_loc.ShareSnapshotInstanceExportLocationController())
self.share = db_utils.create_share()
self.snapshot = db_utils.create_snapshot(
status=constants.STATUS_AVAILABLE,
share_id=self.share['id'])
self.snapshot_instance = db_utils.create_snapshot_instance(
'fake_snapshot_id_1',
status=constants.STATUS_CREATING,
share_instance_id=self.share['instance']['id'])
self.values = {
'share_snapshot_instance_id': self.snapshot_instance['id'],
'path': 'fake/user_path',
'is_admin_only': True,
}
self.el = db_api.share_snapshot_instance_export_location_create(
context.get_admin_context(), self.values)
self.req = self._get_request()
def test_index(self):
self.mock_object(
db_api, 'share_snapshot_instance_export_locations_get_all',
mock.Mock(return_value=[self.el]))
out = self.controller.index(self._get_request('2.32'),
self.snapshot_instance['id'])
values = {
'share_snapshot_export_locations': [{
'share_snapshot_instance_id': self.snapshot_instance['id'],
'path': 'fake/user_path',
'is_admin_only': True,
'id': self.el['id'],
'links': [{
'href': 'http://localhost/v1/fake/'
'share_snapshot_export_locations/' + self.el['id'],
'rel': 'self'
}, {
'href': 'http://localhost/fake/'
'share_snapshot_export_locations/' + self.el['id'],
'rel': 'bookmark'
}],
}]
}
self.assertSubDictMatch(values, out)
def test_show(self):
out = self.controller.show(self._get_request('2.32'),
self.snapshot_instance['id'],
self.el['id'])
self.assertSubDictMatch(
{'share_snapshot_export_location': self.values}, out)
@ddt.data('1.0', '2.0', '2.5', '2.8', '2.31')
def test_list_with_unsupported_version(self, version):
self.assertRaises(
exception.VersionNotFoundForAPIMethod,
self.controller.index,
self._get_request(version),
self.snapshot_instance['id'],
)
@ddt.data('1.0', '2.0', '2.5', '2.8', '2.31')
def test_show_with_unsupported_version(self, version):
self.assertRaises(
exception.VersionNotFoundForAPIMethod,
self.controller.show,
self._get_request(version),
self.snapshot['id'],
self.el['id']
)

View File

@ -32,6 +32,7 @@ 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
from manila import utils
MIN_MANAGE_SNAPSHOT_API_VERSION = '2.12'
@ -327,6 +328,234 @@ class ShareSnapshotAPITest(test.TestCase):
self.assertNotEqual(snp["size"], res_dict['snapshot']["size"])
def test_access_list(self):
share = db_utils.create_share(mount_snapshot_support=True)
snapshot = db_utils.create_snapshot(
status=constants.STATUS_AVAILABLE, share_id=share['id'])
expected = []
self.mock_object(share_api.API, 'get',
mock.Mock(return_value=share))
self.mock_object(share_api.API, 'get_snapshot',
mock.Mock(return_value=snapshot))
self.mock_object(share_api.API, 'snapshot_access_get_all',
mock.Mock(return_value=expected))
id = 'fake_snap_id'
req = fakes.HTTPRequest.blank('/snapshots/%s/action' % id,
version='2.32')
actual = self.controller.access_list(req, id)
self.assertEqual(expected, actual['snapshot_access_list'])
def test_allow_access(self):
share = db_utils.create_share(mount_snapshot_support=True)
snapshot = db_utils.create_snapshot(
status=constants.STATUS_AVAILABLE, share_id=share['id'])
access = {
'id': 'fake_id',
'access_type': 'ip',
'access_to': '1.1.1.1',
'state': 'new',
}
get = self.mock_object(share_api.API, 'get',
mock.Mock(return_value=share))
get_snapshot = self.mock_object(share_api.API, 'get_snapshot',
mock.Mock(return_value=snapshot))
allow_access = self.mock_object(share_api.API, 'snapshot_allow_access',
mock.Mock(return_value=access))
body = {'allow_access': access}
req = fakes.HTTPRequest.blank('/snapshots/%s/action' % snapshot['id'],
version='2.32')
actual = self.controller.allow_access(req, snapshot['id'], body)
self.assertEqual(access, actual['snapshot_access'])
get.assert_called_once_with(utils.IsAMatcher(context.RequestContext),
share['id'])
get_snapshot.assert_called_once_with(
utils.IsAMatcher(context.RequestContext), snapshot['id'])
allow_access.assert_called_once_with(
utils.IsAMatcher(context.RequestContext), snapshot,
access['access_type'], access['access_to'])
def test_allow_access_data_not_found_exception(self):
share = db_utils.create_share(mount_snapshot_support=True)
snapshot = db_utils.create_snapshot(
status=constants.STATUS_AVAILABLE, share_id=share['id'])
req = fakes.HTTPRequest.blank('/snapshots/%s/action' % snapshot['id'],
version='2.32')
body = {}
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.allow_access, req,
snapshot['id'], body)
def test_allow_access_exists_exception(self):
share = db_utils.create_share(mount_snapshot_support=True)
snapshot = db_utils.create_snapshot(
status=constants.STATUS_AVAILABLE, share_id=share['id'])
req = fakes.HTTPRequest.blank('/snapshots/%s/action' % snapshot['id'],
version='2.32')
access = {
'id': 'fake_id',
'access_type': 'ip',
'access_to': '1.1.1.1',
'state': 'new',
}
msg = "Share snapshot access exists."
get = self.mock_object(share_api.API, 'get', mock.Mock(
return_value=share))
get_snapshot = self.mock_object(share_api.API, 'get_snapshot',
mock.Mock(return_value=snapshot))
allow_access = self.mock_object(
share_api.API, 'snapshot_allow_access', mock.Mock(
side_effect=exception.ShareSnapshotAccessExists(msg)))
body = {'allow_access': access}
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.allow_access, req,
snapshot['id'], body)
get.assert_called_once_with(utils.IsAMatcher(context.RequestContext),
share['id'])
get_snapshot.assert_called_once_with(
utils.IsAMatcher(context.RequestContext), snapshot['id'])
allow_access.assert_called_once_with(
utils.IsAMatcher(context.RequestContext), snapshot,
access['access_type'], access['access_to'])
def test_allow_access_share_without_mount_snap_support(self):
share = db_utils.create_share(mount_snapshot_support=False)
snapshot = db_utils.create_snapshot(
status=constants.STATUS_AVAILABLE, share_id=share['id'])
access = {
'id': 'fake_id',
'access_type': 'ip',
'access_to': '1.1.1.1',
'state': 'new',
}
get_snapshot = self.mock_object(share_api.API, 'get_snapshot',
mock.Mock(return_value=snapshot))
get = self.mock_object(share_api.API, 'get',
mock.Mock(return_value=share))
body = {'allow_access': access}
req = fakes.HTTPRequest.blank('/snapshots/%s/action' % snapshot['id'],
version='2.32')
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.allow_access, req,
snapshot['id'], body)
get.assert_called_once_with(utils.IsAMatcher(context.RequestContext),
share['id'])
get_snapshot.assert_called_once_with(
utils.IsAMatcher(context.RequestContext), snapshot['id'])
def test_allow_access_empty_parameters(self):
share = db_utils.create_share(mount_snapshot_support=True)
snapshot = db_utils.create_snapshot(
status=constants.STATUS_AVAILABLE, share_id=share['id'])
access = {'id': 'fake_id',
'access_type': '',
'access_to': ''}
body = {'allow_access': access}
req = fakes.HTTPRequest.blank('/snapshots/%s/action' % snapshot['id'],
version='2.32')
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.allow_access, req,
snapshot['id'], body)
def test_deny_access(self):
share = db_utils.create_share(mount_snapshot_support=True)
snapshot = db_utils.create_snapshot(
status=constants.STATUS_AVAILABLE, share_id=share['id'])
access = db_utils.create_snapshot_access(
share_snapshot_id=snapshot['id'])
get = self.mock_object(share_api.API, 'get',
mock.Mock(return_value=share))
get_snapshot = self.mock_object(share_api.API, 'get_snapshot',
mock.Mock(return_value=snapshot))
access_get = self.mock_object(share_api.API, 'snapshot_access_get',
mock.Mock(return_value=access))
deny_access = self.mock_object(share_api.API, 'snapshot_deny_access')
body = {'deny_access': {'access_id': access.id}}
req = fakes.HTTPRequest.blank('/snapshots/%s/action' % snapshot['id'],
version='2.32')
resp = self.controller.deny_access(req, snapshot['id'], body)
self.assertEqual(202, resp.status_int)
get.assert_called_once_with(utils.IsAMatcher(context.RequestContext),
share['id'])
get_snapshot.assert_called_once_with(
utils.IsAMatcher(context.RequestContext), snapshot['id'])
access_get.assert_called_once_with(
utils.IsAMatcher(context.RequestContext),
body['deny_access']['access_id'])
deny_access.assert_called_once_with(
utils.IsAMatcher(context.RequestContext), snapshot, access)
def test_deny_access_data_not_found_exception(self):
share = db_utils.create_share(mount_snapshot_support=True)
snapshot = db_utils.create_snapshot(
status=constants.STATUS_AVAILABLE, share_id=share['id'])
req = fakes.HTTPRequest.blank('/snapshots/%s/action' % snapshot['id'],
version='2.32')
body = {}
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.deny_access, req,
snapshot['id'], body)
def test_deny_access_access_rule_not_found(self):
share = db_utils.create_share(mount_snapshot_support=True)
snapshot = db_utils.create_snapshot(
status=constants.STATUS_AVAILABLE, share_id=share['id'])
access = db_utils.create_snapshot_access(
share_snapshot_id=snapshot['id'])
wrong_access = {
'access_type': 'fake_type',
'access_to': 'fake_IP',
'share_snapshot_id': 'fake_id'
}
get = self.mock_object(share_api.API, 'get',
mock.Mock(return_value=share))
get_snapshot = self.mock_object(share_api.API, 'get_snapshot',
mock.Mock(return_value=snapshot))
access_get = self.mock_object(share_api.API, 'snapshot_access_get',
mock.Mock(return_value=wrong_access))
body = {'deny_access': {'access_id': access.id}}
req = fakes.HTTPRequest.blank('/snapshots/%s/action' % snapshot['id'],
version='2.32')
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.deny_access, req, snapshot['id'],
body)
get.assert_called_once_with(utils.IsAMatcher(context.RequestContext),
share['id'])
get_snapshot.assert_called_once_with(
utils.IsAMatcher(context.RequestContext), snapshot['id'])
access_get.assert_called_once_with(
utils.IsAMatcher(context.RequestContext),
body['deny_access']['access_id'])
@ddt.ddt
class ShareSnapshotAdminActionsAPITest(test.TestCase):

View File

@ -297,6 +297,7 @@ class ShareTypesAPITest(test.TestCase):
constants.ExtraSpecs.SNAPSHOT_SUPPORT: True,
constants.ExtraSpecs.CREATE_SHARE_FROM_SNAPSHOT_SUPPORT: False,
constants.ExtraSpecs.REVERT_TO_SNAPSHOT_SUPPORT: True,
constants.ExtraSpecs.MOUNT_SNAPSHOT_SUPPORT: True,
}
now = timeutils.utcnow().isoformat()

View File

@ -995,6 +995,25 @@ class ShareSnapshotDatabaseAPITestCase(test.TestCase):
id='fake_snapshot_id_2', share_id=self.share_2['id'],
instances=self.snapshot_instances[3:4])
self.snapshot_instance_export_locations = [
db_utils.create_snapshot_instance_export_locations(
self.snapshot_instances[0].id,
path='1.1.1.1:/fake_path',
is_admin_only=True),
db_utils.create_snapshot_instance_export_locations(
self.snapshot_instances[1].id,
path='2.2.2.2:/fake_path',
is_admin_only=True),
db_utils.create_snapshot_instance_export_locations(
self.snapshot_instances[2].id,
path='3.3.3.3:/fake_path',
is_admin_only=True),
db_utils.create_snapshot_instance_export_locations(
self.snapshot_instances[3].id,
path='4.4.4.4:/fake_path',
is_admin_only=True)
]
def test_create(self):
share = db_utils.create_share(size=1)
values = {
@ -1159,6 +1178,120 @@ class ShareSnapshotDatabaseAPITestCase(test.TestCase):
self.assertEqual(1, len(snapshot['instances']))
self.assertEqual(first_instance_id, snapshot['instance']['id'])
def test_share_snapshot_access_create(self):
values = {
'share_snapshot_id': self.snapshot_1['id'],
}
actual_result = db_api.share_snapshot_access_create(self.ctxt,
values)
self.assertSubDictMatch(values, actual_result.to_dict())
def test_share_snapshot_instance_access_get_all(self):
access = db_utils.create_snapshot_access(
share_snapshot_id=self.snapshot_1['id'])
session = db_api.get_session()
values = {'share_snapshot_instance_id': self.snapshot_instances[0].id,
'access_id': access['id']}
rules = db_api.share_snapshot_instance_access_get_all(
self.ctxt, access['id'], session)
self.assertSubDictMatch(values, rules[0].to_dict())
def test_share_snapshot_access_get(self):
access = db_utils.create_snapshot_access(
share_snapshot_id=self.snapshot_1['id'])
values = {'share_snapshot_id': self.snapshot_1['id']}
actual_value = db_api.share_snapshot_access_get(
self.ctxt, access['id'])
self.assertSubDictMatch(values, actual_value.to_dict())
def test_share_snapshot_access_get_all_for_share_snapshot(self):
access = db_utils.create_snapshot_access(
share_snapshot_id=self.snapshot_1['id'])
values = {'access_type': access['access_type'],
'access_to': access['access_to'],
'share_snapshot_id': self.snapshot_1['id']}
actual_value = db_api.share_snapshot_access_get_all_for_share_snapshot(
self.ctxt, self.snapshot_1['id'], {})
self.assertSubDictMatch(values, actual_value[0].to_dict())
def test_share_snapshot_access_get_all_for_snapshot_instance(self):
access = db_utils.create_snapshot_access(
share_snapshot_id=self.snapshot_1['id'])
values = {'access_type': access['access_type'],
'access_to': access['access_to'],
'share_snapshot_id': self.snapshot_1['id']}
out = db_api.share_snapshot_access_get_all_for_snapshot_instance(
self.ctxt, self.snapshot_instances[0].id)
self.assertSubDictMatch(values, out[0].to_dict())
def test_share_snapshot_instance_access_update_state(self):
access = db_utils.create_snapshot_access(
share_snapshot_id=self.snapshot_1['id'])
values = {'state': constants.STATUS_ACTIVE,
'access_id': access['id'],
'share_snapshot_instance_id': self.snapshot_instances[0].id}
actual_result = db_api.share_snapshot_instance_access_update(
self.ctxt, access['id'], self.snapshot_1.instance['id'],
{'state': constants.STATUS_ACTIVE})
self.assertSubDictMatch(values, actual_result.to_dict())
def test_share_snapshot_instance_access_get(self):
access = db_utils.create_snapshot_access(
share_snapshot_id=self.snapshot_1['id'])
values = {'access_id': access['id'],
'share_snapshot_instance_id': self.snapshot_instances[0].id}
actual_result = db_api.share_snapshot_instance_access_get(
self.ctxt, access['id'], self.snapshot_instances[0].id)
self.assertSubDictMatch(values, actual_result.to_dict())
def test_share_snapshot_instance_access_delete(self):
access = db_utils.create_snapshot_access(
share_snapshot_id=self.snapshot_1['id'])
db_api.share_snapshot_instance_access_delete(
self.ctxt, access['id'], self.snapshot_1.instance['id'])
def test_share_snapshot_instance_export_location_create(self):
values = {
'share_snapshot_instance_id': self.snapshot_instances[0].id,
}
actual_result = db_api.share_snapshot_instance_export_location_create(
self.ctxt, values)
self.assertSubDictMatch(values, actual_result.to_dict())
def test_share_snapshot_export_locations_get(self):
out = db_api.share_snapshot_export_locations_get(
self.ctxt, self.snapshot_1['id'])
keys = ['share_snapshot_instance_id', 'path', 'is_admin_only']
for expected, actual in zip(self.snapshot_instance_export_locations,
out):
[self.assertEqual(expected[k], actual[k]) for k in keys]
def test_share_snapshot_instance_export_locations_get(self):
out = db_api.share_snapshot_instance_export_locations_get_all(
self.ctxt, self.snapshot_instances[0].id)
keys = ['share_snapshot_instance_id', 'path', 'is_admin_only']
for key in keys:
self.assertEqual(self.snapshot_instance_export_locations[0][key],
out[0][key])
class ShareExportLocationsDatabaseAPITestCase(test.TestCase):

View File

@ -167,6 +167,17 @@ def create_snapshot_instance(snapshot_id, **kwargs):
context.get_admin_context(), snapshot_id, snapshot_instance)
def create_snapshot_instance_export_locations(snapshot_id, **kwargs):
"""Create a snapshot instance export location object."""
export_location = {
'share_snapshot_instance_id': snapshot_id,
}
export_location.update(kwargs)
return db.share_snapshot_instance_export_location_create(
context.get_admin_context(), export_location)
def create_access(**kwargs):
"""Create a access rule object."""
state = kwargs.pop('state', constants.ACCESS_STATE_QUEUED_TO_APPLY)
@ -186,6 +197,16 @@ def create_access(**kwargs):
return share_access_rule
def create_snapshot_access(**kwargs):
"""Create a snapshot access rule object."""
access = {
'access_type': 'fake_type',
'access_to': 'fake_IP',
'share_snapshot_id': None,
}
return _create_db_row(db.share_snapshot_access_create, access, kwargs)
def create_share_server(**kwargs):
"""Create a share server object."""
backend_details = kwargs.pop('backend_details', {})

View File

@ -40,6 +40,7 @@ def fake_share(**kwargs):
'is_busy': False,
'share_group_id': None,
'instance': {'host': 'fakehost'},
'mount_snapshot_support': False,
}
share.update(kwargs)
return db_fakes.FakeModel(share)

View File

@ -41,6 +41,7 @@ SERVICE_STATES_NO_POOLS = {
snapshot_support=False,
create_share_from_snapshot_support=False,
revert_to_snapshot_support=True,
mount_snapshot_support=True,
driver_handles_share_servers=False),
'host2@back1': dict(share_backend_name='BBB',
total_capacity_gb=256, free_capacity_gb=100,
@ -51,6 +52,7 @@ SERVICE_STATES_NO_POOLS = {
snapshot_support=True,
create_share_from_snapshot_support=True,
revert_to_snapshot_support=False,
mount_snapshot_support=False,
driver_handles_share_servers=False),
'host2@back2': dict(share_backend_name='CCC',
total_capacity_gb=10000, free_capacity_gb=700,
@ -61,6 +63,7 @@ SERVICE_STATES_NO_POOLS = {
snapshot_support=True,
create_share_from_snapshot_support=True,
revert_to_snapshot_support=False,
mount_snapshot_support=False,
driver_handles_share_servers=False),
}

View File

@ -212,6 +212,7 @@ class HostManagerTestCase(test.TestCase):
'snapshot_support': False,
'create_share_from_snapshot_support': False,
'revert_to_snapshot_support': True,
'mount_snapshot_support': True,
'dedupe': False,
'compression': False,
'replication_type': None,
@ -238,6 +239,7 @@ class HostManagerTestCase(test.TestCase):
'snapshot_support': True,
'create_share_from_snapshot_support': True,
'revert_to_snapshot_support': False,
'mount_snapshot_support': False,
'dedupe': False,
'compression': False,
'replication_type': None,
@ -264,6 +266,7 @@ class HostManagerTestCase(test.TestCase):
'snapshot_support': True,
'create_share_from_snapshot_support': True,
'revert_to_snapshot_support': False,
'mount_snapshot_support': False,
'dedupe': False,
'compression': False,
'replication_type': None,
@ -312,6 +315,7 @@ class HostManagerTestCase(test.TestCase):
'snapshot_support': True,
'create_share_from_snapshot_support': True,
'revert_to_snapshot_support': True,
'mount_snapshot_support': False,
'dedupe': False,
'compression': False,
'replication_type': None,
@ -339,6 +343,7 @@ class HostManagerTestCase(test.TestCase):
'snapshot_support': True,
'create_share_from_snapshot_support': True,
'revert_to_snapshot_support': False,
'mount_snapshot_support': False,
'dedupe': False,
'compression': False,
'replication_type': None,
@ -366,6 +371,7 @@ class HostManagerTestCase(test.TestCase):
'snapshot_support': True,
'create_share_from_snapshot_support': True,
'revert_to_snapshot_support': False,
'mount_snapshot_support': False,
'dedupe': False,
'compression': False,
'replication_type': None,
@ -393,6 +399,7 @@ class HostManagerTestCase(test.TestCase):
'snapshot_support': True,
'create_share_from_snapshot_support': True,
'revert_to_snapshot_support': False,
'mount_snapshot_support': False,
'dedupe': False,
'compression': False,
'replication_type': None,
@ -420,6 +427,7 @@ class HostManagerTestCase(test.TestCase):
'snapshot_support': True,
'create_share_from_snapshot_support': True,
'revert_to_snapshot_support': False,
'mount_snapshot_support': False,
'dedupe': False,
'compression': False,
'replication_type': None,
@ -470,6 +478,7 @@ class HostManagerTestCase(test.TestCase):
'snapshot_support': False,
'create_share_from_snapshot_support': False,
'revert_to_snapshot_support': True,
'mount_snapshot_support': True,
'share_backend_name': 'AAA',
'free_capacity_gb': 200,
'driver_version': None,
@ -496,6 +505,7 @@ class HostManagerTestCase(test.TestCase):
'snapshot_support': True,
'create_share_from_snapshot_support': True,
'revert_to_snapshot_support': False,
'mount_snapshot_support': False,
'share_backend_name': 'BBB',
'free_capacity_gb': 100,
'driver_version': None,
@ -550,6 +560,7 @@ class HostManagerTestCase(test.TestCase):
'snapshot_support': True,
'create_share_from_snapshot_support': True,
'revert_to_snapshot_support': False,
'mount_snapshot_support': False,
'share_backend_name': 'BBB',
'free_capacity_gb': 42,
'driver_version': None,

View File

@ -127,6 +127,7 @@ class EMCShareFrameworkTestCase(test.TestCase):
data['create_share_from_snapshot_support'] = True
data['revert_to_snapshot_support'] = False
data['share_group_snapshot_support'] = True
data['mount_snapshot_support'] = False
data['replication_domain'] = None
data['filter_function'] = None
data['goodness_function'] = None

View File

@ -229,19 +229,25 @@ class DummyDriver(driver.ShareDriver):
"""Is called to create share from snapshot."""
return self._create_share(share, share_server=share_server)
def _create_snapshot(self, snapshot):
def _create_snapshot(self, snapshot, share_server=None):
snapshot_name = self._get_snapshot_name(snapshot)
mountpoint = "/path/to/fake/snapshot/%s" % snapshot_name
self.private_storage.update(
snapshot["id"], {
"fake_provider_snapshot_name": snapshot_name,
"fake_provider_location": mountpoint,
}
)
return {"provider_location": snapshot_name}
return {
"provider_location": mountpoint,
"export_locations": self._generate_export_locations(
mountpoint, share_server=share_server)
}
@slow_me_down
def create_snapshot(self, context, snapshot, share_server=None):
"""Is called to create snapshot."""
return self._create_snapshot(snapshot)
return self._create_snapshot(snapshot, share_server)
@slow_me_down
def delete_share(self, context, share, share_server=None):
@ -278,6 +284,13 @@ class DummyDriver(driver.ShareDriver):
"access_type": access_type, "share_proto": share_proto}
raise exception.InvalidShareAccess(reason=msg)
@slow_me_down
def snapshot_update_access(self, context, snapshot, access_rules,
add_rules, delete_rules, share_server=None):
"""Update access rules for given snapshot."""
self.update_access(context, snapshot['share'], access_rules,
add_rules, delete_rules, share_server)
@slow_me_down
def do_setup(self, context):
"""Any initialization the share driver does while starting."""
@ -366,6 +379,7 @@ class DummyDriver(driver.ShareDriver):
"snapshot_support": True,
"create_share_from_snapshot_support": True,
"revert_to_snapshot_support": True,
"mount_snapshot_support": True,
"driver_name": "Dummy",
"pools": self._get_pools_info(),
}

View File

@ -259,6 +259,7 @@ class GlusterfsNativeShareDriverTestCase(test.TestCase):
'create_share_from_snapshot_support': True,
'revert_to_snapshot_support': False,
'share_group_snapshot_support': True,
'mount_snapshot_support': False,
'replication_domain': None,
'filter_function': None,
'goodness_function': None,

View File

@ -736,6 +736,7 @@ class HPE3ParDriverTestCase(test.TestCase):
'create_share_from_snapshot_support': True,
'revert_to_snapshot_support': False,
'share_group_snapshot_support': True,
'mount_snapshot_support': False,
'storage_protocol': 'NFS_CIFS',
'thin_provisioning': True,
'total_capacity_gb': 0,
@ -813,6 +814,7 @@ class HPE3ParDriverTestCase(test.TestCase):
'create_share_from_snapshot_support': True,
'revert_to_snapshot_support': False,
'share_group_snapshot_support': True,
'mount_snapshot_support': False,
'replication_domain': None,
'filter_function': None,
'goodness_function': None,
@ -852,6 +854,7 @@ class HPE3ParDriverTestCase(test.TestCase):
'create_share_from_snapshot_support': True,
'revert_to_snapshot_support': False,
'share_group_snapshot_support': True,
'mount_snapshot_support': False,
'replication_domain': None,
'filter_function': None,
'goodness_function': None,

View File

@ -2426,6 +2426,7 @@ class HuaweiShareDriverTestCase(test.TestCase):
"create_share_from_snapshot_support": snapshot_support,
"revert_to_snapshot_support": False,
"share_group_snapshot_support": True,
"mount_snapshot_support": False,
"replication_domain": None,
"filter_function": None,
"goodness_function": None,

View File

@ -57,7 +57,8 @@ def fake_snapshot(**kwargs):
'share': {
'id': 'fakeid',
'name': 'fakename',
'size': 1
'size': 1,
'share_proto': 'NFS',
},
}
snapshot.update(kwargs)
@ -324,10 +325,14 @@ class LVMShareDriverTestCase(test.TestCase):
def test_create_snapshot(self):
self._driver.create_snapshot(self._context, self.snapshot,
self.share_server)
mount_path = self._get_mount_path(self.snapshot)
expected_exec = [
"lvcreate -L 1G --name %s --snapshot %s/fakename" % (
self.snapshot['name'], CONF.lvm_share_volume_group,),
("lvcreate -L 1G --name fakesnapshotname --snapshot "
"%s/fakename" % (CONF.lvm_share_volume_group,)),
"tune2fs -U random /dev/mapper/fakevg-%s" % self.snapshot['name'],
"mkdir -p " + mount_path,
"mount /dev/mapper/fakevg-fakesnapshotname " + mount_path,
"chmod 777 " + mount_path,
]
self.assertEqual(expected_exec, fake_utils.fake_execute_get_log())
@ -349,7 +354,10 @@ class LVMShareDriverTestCase(test.TestCase):
self._driver._delete_share(self._context, self.share)
def test_delete_snapshot(self):
expected_exec = ['lvremove -f fakevg/fakesnapshotname']
expected_exec = [
'umount -f ' + self._get_mount_path(self.snapshot),
'lvremove -f fakevg/fakesnapshotname',
]
self._driver.delete_snapshot(self._context, self.snapshot,
self.share_server)
self.assertEqual(expected_exec, fake_utils.fake_execute_get_log())
@ -529,20 +537,54 @@ class LVMShareDriverTestCase(test.TestCase):
self.share_server)
snap_lv = "%s/fakesnapshotname" % (CONF.lvm_share_volume_group)
share_lv = "%s/fakename" % (CONF.lvm_share_volume_group)
mount_path = self._get_mount_path(self.snapshot['share'])
share_mount_path = self._get_mount_path(self.snapshot['share'])
snapshot_mount_path = self._get_mount_path(self.snapshot)
expected_exec = [
('umount -f %s' % snapshot_mount_path),
("lvconvert --merge %s" % snap_lv),
("umount %s" % mount_path),
("rmdir %s" % mount_path),
("umount %s" % share_mount_path),
("rmdir %s" % share_mount_path),
("lvchange -an %s" % share_lv),
("lvchange -ay %s" % share_lv),
("lvcreate -L 1G --name fakesnapshotname --snapshot %s" %
share_lv),
('tune2fs -U random /dev/mapper/%s-fakesnapshotname' %
CONF.lvm_share_volume_group),
("mkdir -p %s" % mount_path),
("mkdir -p %s" % share_mount_path),
("mount /dev/mapper/%s-fakename %s" %
(CONF.lvm_share_volume_group, mount_path)),
("chmod 777 %s" % mount_path),
(CONF.lvm_share_volume_group, share_mount_path)),
("chmod 777 %s" % share_mount_path),
("mkdir -p %s" % snapshot_mount_path),
("mount /dev/mapper/fakevg-fakesnapshotname "
"%s" % snapshot_mount_path),
("chmod 777 %s" % snapshot_mount_path),
]
self.assertEqual(expected_exec, fake_utils.fake_execute_get_log())
def test_snapshot_update_access(self):
access_rules = [{
'access_type': 'ip',
'access_to': '1.1.1.1',
'access_level': 'ro',
}]
add_rules = [{
'access_type': 'ip',
'access_to': '2.2.2.2',
'access_level': 'ro',
}]
delete_rules = [{
'access_type': 'ip',
'access_to': '3.3.3.3',
'access_level': 'ro',
}]
self._driver.snapshot_update_access(self._context, self.snapshot,
access_rules, add_rules,
delete_rules)
(self._driver._helpers[self.snapshot['share']['share_proto']].
update_access.assert_called_once_with(
self.server, self.snapshot['name'],
access_rules, add_rules=add_rules, delete_rules=delete_rules))

View File

@ -356,6 +356,7 @@ class ZFSonLinuxShareDriverTestCase(test.TestCase):
'create_share_from_snapshot_support': True,
'revert_to_snapshot_support': False,
'share_group_snapshot_support': True,
'mount_snapshot_support': False,
'storage_protocol': 'NFS',
'total_capacity_gb': 'unknown',
'vendor_name': 'Open Source',

View File

@ -752,6 +752,7 @@ class ShareAPITestCase(test.TestCase):
'snapshot_support': True,
'create_share_from_snapshot_support': False,
'revert_to_snapshot_support': False,
'mount_snapshot_support': False,
'replication_type': 'dr',
}
}
@ -769,6 +770,7 @@ class ShareAPITestCase(test.TestCase):
'snapshot_support': False,
'create_share_from_snapshot_support': False,
'revert_to_snapshot_support': False,
'mount_snapshot_support': False,
'replication_type': None,
}
self.assertEqual(expected, result)
@ -803,6 +805,7 @@ class ShareAPITestCase(test.TestCase):
'replication_type': replication_type,
'create_share_from_snapshot_support': False,
'revert_to_snapshot_support': False,
'mount_snapshot_support': False,
},
}
@ -831,6 +834,8 @@ class ShareAPITestCase(test.TestCase):
fake_type['extra_specs']['create_share_from_snapshot_support'],
'revert_to_snapshot_support':
fake_type['extra_specs']['revert_to_snapshot_support'],
'mount_snapshot_support':
fake_type['extra_specs']['mount_snapshot_support'],
'replication_type': replication_type,
})
@ -893,6 +898,9 @@ class ShareAPITestCase(test.TestCase):
'revert_to_snapshot_support': kwargs.get(
'revert_to_snapshot_support',
share_type['extra_specs'].get('revert_to_snapshot_support')),
'mount_snapshot_support': kwargs.get(
'mount_snapshot_support',
share_type['extra_specs'].get('mount_snapshot_support')),
'share_proto': kwargs.get('share_proto', share.get('share_proto')),
'share_type_id': share_type['id'],
'is_public': kwargs.get('is_public', share.get('is_public')),
@ -2232,6 +2240,169 @@ class ShareAPITestCase(test.TestCase):
self.context, share, new_size
)
def test_snapshot_allow_access(self):
access_to = '1.1.1.1'
access_type = 'ip'
share = db_utils.create_share()
snapshot = db_utils.create_snapshot(share_id=share['id'],
status=constants.STATUS_AVAILABLE)
access = db_utils.create_snapshot_access(
share_snapshot_id=snapshot['id'])
filters = {'access_to': access_to,
'access_type': access_type}
values = {'share_snapshot_id': snapshot['id'],
'access_type': access_type,
'access_to': access_to}
access_get_all = self.mock_object(
db_api, 'share_snapshot_access_get_all_for_share_snapshot',
mock.Mock(return_value=[]))
access_create = self.mock_object(
db_api, 'share_snapshot_access_create',
mock.Mock(return_value=access))
self.mock_object(self.api.share_rpcapi, 'snapshot_update_access')
out = self.api.snapshot_allow_access(self.context, snapshot,
access_type, access_to)
self.assertEqual(access, out)
access_get_all.assert_called_once_with(
utils.IsAMatcher(context.RequestContext), snapshot['id'], filters)
access_create.assert_called_once_with(
utils.IsAMatcher(context.RequestContext), values)
def test_snapshot_allow_access_instance_exception(self):
access_to = '1.1.1.1'
access_type = 'ip'
share = db_utils.create_share()
snapshot = db_utils.create_snapshot(share_id=share['id'])
filters = {'access_to': access_to,
'access_type': access_type}
access_get_all = self.mock_object(
db_api, 'share_snapshot_access_get_all_for_share_snapshot',
mock.Mock(return_value=[]))
self.assertRaises(exception.InvalidShareSnapshotInstance,
self.api.snapshot_allow_access, self.context,
snapshot, access_type, access_to)
access_get_all.assert_called_once_with(
utils.IsAMatcher(context.RequestContext), snapshot['id'], filters)
def test_snapshot_allow_access_access_exists_exception(self):
access_to = '1.1.1.1'
access_type = 'ip'
share = db_utils.create_share()
snapshot = db_utils.create_snapshot(share_id=share['id'])
access = db_utils.create_snapshot_access(
share_snapshot_id=snapshot['id'])
filters = {'access_to': access_to,
'access_type': access_type}
access_get_all = self.mock_object(
db_api, 'share_snapshot_access_get_all_for_share_snapshot',
mock.Mock(return_value=[access]))
self.assertRaises(exception.ShareSnapshotAccessExists,
self.api.snapshot_allow_access, self.context,
snapshot, access_type, access_to)
access_get_all.assert_called_once_with(
utils.IsAMatcher(context.RequestContext), snapshot['id'], filters)
def test_snapshot_deny_access(self):
share = db_utils.create_share()
snapshot = db_utils.create_snapshot(share_id=share['id'],
status=constants.STATUS_AVAILABLE)
access = db_utils.create_snapshot_access(
share_snapshot_id=snapshot['id'])
mapping = {'id': 'fake_id',
'state': constants.STATUS_ACTIVE,
'access_id': access['id']}
access_get = self.mock_object(
db_api, 'share_snapshot_instance_access_get',
mock.Mock(return_value=mapping))
access_update_state = self.mock_object(
db_api, 'share_snapshot_instance_access_update')
update_access = self.mock_object(self.api.share_rpcapi,
'snapshot_update_access')
self.api.snapshot_deny_access(self.context, snapshot, access)
access_get.assert_called_once_with(
utils.IsAMatcher(context.RequestContext), access['id'],
snapshot['instance']['id'])
access_update_state.assert_called_once_with(
utils.IsAMatcher(context.RequestContext), access['id'],
snapshot.instance['id'],
{'state': constants.ACCESS_STATE_QUEUED_TO_DENY})
update_access.assert_called_once_with(
utils.IsAMatcher(context.RequestContext), snapshot['instance'])
def test_snapshot_deny_access_exception(self):
share = db_utils.create_share()
snapshot = db_utils.create_snapshot(share_id=share['id'])
access = db_utils.create_snapshot_access(
share_snapshot_id=snapshot['id'])
self.assertRaises(exception.InvalidShareSnapshotInstance,
self.api.snapshot_deny_access, self.context,
snapshot, access)
def test_snapshot_access_get_all(self):
share = db_utils.create_share()
snapshot = db_utils.create_snapshot(share_id=share['id'])
access = []
access.append(db_utils.create_snapshot_access(
share_snapshot_id=snapshot['id']))
self.mock_object(
db_api, 'share_snapshot_access_get_all_for_share_snapshot',
mock.Mock(return_value=access))
out = self.api.snapshot_access_get_all(self.context, snapshot)
self.assertEqual(access, out)
def test_snapshot_access_get(self):
share = db_utils.create_share()
snapshot = db_utils.create_snapshot(share_id=share['id'])
access = db_utils.create_snapshot_access(
share_snapshot_id=snapshot['id'])
self.mock_object(
db_api, 'share_snapshot_access_get',
mock.Mock(return_value=access))
out = self.api.snapshot_access_get(self.context, access['id'])
self.assertEqual(access, out)
def test_snapshot_export_locations_get(self):
share = db_utils.create_share()
snapshot = db_utils.create_snapshot(share_id=share['id'])
self.mock_object(
db_api, 'share_snapshot_export_locations_get',
mock.Mock(return_value=''))
out = self.api.snapshot_export_locations_get(self.context, snapshot)
self.assertEqual('', out)
def test_snapshot_export_location_get(self):
fake_el = '/fake_export_location'
self.mock_object(
db_api, 'share_snapshot_instance_export_location_get',
mock.Mock(return_value=fake_el))
out = self.api.snapshot_export_location_get(self.context, 'fake_id')
self.assertEqual(fake_el, out)
@ddt.data({'share_type': True, 'share_net': True, 'dhss': True},
{'share_type': False, 'share_net': True, 'dhss': True},
{'share_type': False, 'share_net': False, 'dhss': True},
@ -2253,6 +2424,7 @@ class ShareAPITestCase(test.TestCase):
'snapshot_support': False,
'create_share_from_snapshot_support': False,
'revert_to_snapshot_support': False,
'mount_snapshot_support': False,
'driver_handles_share_servers': dhss,
},
}
@ -2264,6 +2436,7 @@ class ShareAPITestCase(test.TestCase):
'snapshot_support': False,
'create_share_from_snapshot_support': False,
'revert_to_snapshot_support': False,
'mount_snapshot_support': False,
'driver_handles_share_servers': dhss,
},
}

View File

@ -139,7 +139,7 @@ class ShareDriverTestCase(test.TestCase):
'free_capacity_gb', 'total_capacity_gb',
'driver_handles_share_servers',
'reserved_percentage', 'vendor_name', 'storage_protocol',
'snapshot_support',
'snapshot_support', 'mount_snapshot_support',
]
share_driver = driver.ShareDriver(True, configuration=conf)
fake_stats = {'fake_key': 'fake_value'}
@ -1009,3 +1009,10 @@ class ShareDriverTestCase(test.TestCase):
])
self.assertIsNone(share_group_snapshot_update)
self.assertIsNone(member_update_list)
def test_snapshot_update_access(self):
share_driver = self._instantiate_share_driver(None, False)
self.assertRaises(NotImplementedError,
share_driver.snapshot_update_access,
'fake_context', 'fake_snapshot', ['r1', 'r2'],
[], [])

View File

@ -1500,13 +1500,17 @@ class ShareManagerTestCase(test.TestCase):
def test_delete_snapshot_driver_exception(self, exc):
share_id = 'FAKE_SHARE_ID'
share = fakes.fake_share(id=share_id, instance={'id': 'fake_id'})
share = fakes.fake_share(id=share_id, instance={'id': 'fake_id'},
mount_snapshot_support=True)
snapshot_instance = fakes.fake_snapshot_instance(
share_id=share_id, share=share, name='fake_snapshot')
snapshot = fakes.fake_snapshot(
share_id=share_id, share=share, instance=snapshot_instance,
project_id=self.context.project_id)
snapshot_id = snapshot['id']
update_access = self.mock_object(
self.share_manager.snapshot_access_helper, 'update_access_rules')
self.mock_object(self.share_manager.driver, "delete_snapshot",
mock.Mock(side_effect=exc))
self.mock_object(self.share_manager, '_get_share_server',
@ -1532,6 +1536,9 @@ class ShareManagerTestCase(test.TestCase):
self.share_manager.driver.delete_snapshot.assert_called_once_with(
mock.ANY, expected_snapshot_instance_dict,
share_server=None)
update_access.assert_called_once_with(
utils.IsAMatcher(context.RequestContext),
snapshot_instance['id'], delete_all_rules=True, share_server=None)
self.assertFalse(db_destroy_call.called)
self.assertFalse(mock_exception_log.called)
@ -5007,7 +5014,11 @@ class ShareManagerTestCase(test.TestCase):
@ddt.data(
{'size': 1},
{'size': 2, 'name': 'fake'},
{'size': 3})
{'size': 3},
{'size': 3, 'export_locations': [{'path': '/path1',
'is_admin_only': True},
{'path': '/path2',
'is_admin_only': False}]})
def test_manage_snapshot_valid_snapshot(self, driver_data):
mock_get_share_server = self.mock_object(self.share_manager,
'_get_share_server',
@ -5028,6 +5039,8 @@ class ShareManagerTestCase(test.TestCase):
mock_get = self.mock_object(self.share_manager.db,
'share_snapshot_get',
mock.Mock(return_value=snapshot))
self.mock_object(self.share_manager.db,
'share_snapshot_instance_export_location_create')
self.share_manager.manage_snapshot(self.context, snapshot_id,
driver_options)
@ -5087,6 +5100,7 @@ class ShareManagerTestCase(test.TestCase):
utils.IsAMatcher(context.RequestContext), snapshot['share'])
def test_unmanage_snapshot_invalid_share(self):
manager.CONF.unmanage_remove_access_rules = False
self.mock_object(self.share_manager, 'driver')
self.share_manager.driver.driver_handles_share_servers = False
mock_unmanage = mock.Mock(
@ -5121,9 +5135,12 @@ class ShareManagerTestCase(test.TestCase):
if quota_error:
self.mock_object(quota.QUOTAS, 'reserve', mock.Mock(
side_effect=exception.ManilaException(message='error')))
manager.CONF.unmanage_remove_access_rules = True
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
mock_update_access = self.mock_object(
self.share_manager.snapshot_access_helper, "update_access_rules")
self.mock_object(self.share_manager.driver, "unmanage_snapshot")
mock_get_share_server = self.mock_object(
self.share_manager,
@ -5136,17 +5153,26 @@ class ShareManagerTestCase(test.TestCase):
mock_get = self.mock_object(self.share_manager.db,
'share_snapshot_get',
mock.Mock(return_value=snapshot))
mock_snap_ins_get = self.mock_object(
self.share_manager.db, 'share_snapshot_instance_get',
mock.Mock(return_value=snapshot.instance))
self.share_manager.unmanage_snapshot(self.context, snapshot['id'])
self.share_manager.driver.unmanage_snapshot.assert_called_once_with(
mock.ANY)
snapshot.instance)
mock_update_access.assert_called_once_with(
utils.IsAMatcher(context.RequestContext), snapshot.instance['id'],
delete_all_rules=True, share_server=None)
mock_snapshot_instance_destroy_call.assert_called_once_with(
mock.ANY, snapshot['instance']['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'])
mock_snap_ins_get.assert_called_once_with(
utils.IsAMatcher(context.RequestContext), snapshot.instance['id'],
with_share_data=True)
if quota_error:
self.assertTrue(mock_log_warning.called)
@ -5306,6 +5332,69 @@ class ShareManagerTestCase(test.TestCase):
mock.ANY, 'fake_snapshot_id',
{'status': constants.STATUS_AVAILABLE})
def test_unmanage_snapshot_update_access_rule_exception(self):
self.mock_object(self.share_manager, 'driver')
self.share_manager.driver.driver_handles_share_servers = False
share = db_utils.create_share()
snapshot = db_utils.create_snapshot(share_id=share['id'])
manager.CONF.unmanage_remove_access_rules = True
mock_get = self.mock_object(
self.share_manager.db, 'share_snapshot_get',
mock.Mock(return_value=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.snapshot_access_helper,
'update_access_rules',
mock.Mock(side_effect=Exception))
mock_log_exception = self.mock_object(manager.LOG, 'exception')
mock_update = self.mock_object(self.share_manager.db,
'share_snapshot_update')
self.share_manager.unmanage_snapshot(self.context, snapshot['id'])
self.assertTrue(mock_log_exception.called)
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'])
mock_update.assert_called_once_with(
utils.IsAMatcher(context.RequestContext), snapshot['id'],
{'status': constants.STATUS_UNMANAGE_ERROR})
def test_snapshot_update_access(self):
snapshot = fakes.fake_snapshot(create_instance=True)
snapshot_instance = fakes.fake_snapshot_instance(
base_snapshot=snapshot)
mock_instance_get = self.mock_object(
db, 'share_snapshot_instance_get',
mock.Mock(return_value=snapshot_instance))
mock_get_share_server = self.mock_object(self.share_manager,
'_get_share_server',
mock.Mock(return_value=None))
mock_update_access = self.mock_object(
self.share_manager.snapshot_access_helper, 'update_access_rules')
self.share_manager.snapshot_update_access(self.context,
snapshot_instance['id'])
mock_instance_get.assert_called_once_with(
utils.IsAMatcher(context.RequestContext),
snapshot_instance['id'], with_share_data=True)
mock_get_share_server.assert_called_once_with(
utils.IsAMatcher(context.RequestContext),
snapshot_instance['share_instance'])
mock_update_access.assert_called_once_with(
utils.IsAMatcher(context.RequestContext),
snapshot_instance['id'], share_server=None)
def _setup_crud_replicated_snapshot_data(self):
snapshot = fakes.fake_snapshot(create_instance=True)
snapshot_instance = fakes.fake_snapshot_instance(

View File

@ -55,6 +55,8 @@ class ShareRpcAPITestCase(test.TestCase):
self.fake_share['instance'] = jsonutils.to_primitive(share.instance)
self.fake_share_replica = jsonutils.to_primitive(share_replica)
self.fake_snapshot = jsonutils.to_primitive(snapshot)
self.fake_snapshot['share_instance'] = jsonutils.to_primitive(
snapshot.instance)
self.fake_share_server = jsonutils.to_primitive(share_server)
self.fake_share_group = jsonutils.to_primitive(share_group)
self.fake_share_group_snapshot = jsonutils.to_primitive(
@ -111,6 +113,9 @@ class ShareRpcAPITestCase(test.TestCase):
if 'update_access' in expected_msg:
share_instance = expected_msg.pop('share_instance', None)
expected_msg['share_instance_id'] = share_instance['id']
if 'snapshot_instance' in expected_msg:
snapshot_instance = expected_msg.pop('snapshot_instance', None)
expected_msg['snapshot_instance_id'] = snapshot_instance['id']
if 'host' in kwargs:
host = kwargs['host']
@ -357,3 +362,10 @@ class ShareRpcAPITestCase(test.TestCase):
version='1.12',
share_instance=self.fake_share['instance'],
share_server_id='fake_server_id')
def test_snapshot_update_access(self):
self._test_share_api('snapshot_update_access',
rpc_method='cast',
version='1.17',
snapshot_instance=self.fake_snapshot[
'share_instance'])

View File

@ -239,7 +239,8 @@ class ShareTypesTestCase(test.TestCase):
list(itertools.product(
(constants.ExtraSpecs.SNAPSHOT_SUPPORT,
constants.ExtraSpecs.CREATE_SHARE_FROM_SNAPSHOT_SUPPORT,
constants.ExtraSpecs.REVERT_TO_SNAPSHOT_SUPPORT),
constants.ExtraSpecs.REVERT_TO_SNAPSHOT_SUPPORT,
constants.ExtraSpecs.MOUNT_SNAPSHOT_SUPPORT),
strutils.TRUE_STRINGS + strutils.FALSE_STRINGS))) +
list(itertools.product(
(constants.ExtraSpecs.REPLICATION_TYPE_SPEC,),

View File

@ -0,0 +1,161 @@
# Copyright (c) 2016 Hitachi Data Systems, Inc.
# 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 manila.common import constants
from manila import context
from manila import db
from manila import exception
from manila.share import snapshot_access
from manila import test
from manila.tests import db_utils
from manila import utils
@ddt.ddt
class SnapshotAccessTestCase(test.TestCase):
def setUp(self):
super(SnapshotAccessTestCase, self).setUp()
self.driver = self.mock_class("manila.share.driver.ShareDriver",
mock.Mock())
self.snapshot_access = snapshot_access.ShareSnapshotInstanceAccess(
db, self.driver)
self.context = context.get_admin_context()
share = db_utils.create_share()
self.snapshot = db_utils.create_snapshot(share_id=share['id'])
self.snapshot_instance = db_utils.create_snapshot_instance(
snapshot_id=self.snapshot['id'],
share_instance_id=self.snapshot['share']['instance']['id'])
@ddt.data(constants.ACCESS_STATE_QUEUED_TO_APPLY,
constants.ACCESS_STATE_QUEUED_TO_DENY)
def test_update_access_rules(self, state):
rules = []
for i in range(2):
rules.append({
'id': 'id-%s' % i,
'state': state,
'access_id': 'rule_id%s' % i
})
snapshot_instance_get = self.mock_object(
db, 'share_snapshot_instance_get',
mock.Mock(return_value=self.snapshot_instance))
snap_get_all_for_snap_instance = self.mock_object(
db, 'share_snapshot_access_get_all_for_snapshot_instance',
mock.Mock(return_value=rules))
self.mock_object(db, 'share_snapshot_instance_access_update')
self.mock_object(self.driver, 'snapshot_update_access')
self.mock_object(self.snapshot_access, '_check_needs_refresh',
mock.Mock(return_value=False))
self.mock_object(db, 'share_snapshot_instance_access_delete')
self.snapshot_access.update_access_rules(self.context,
self.snapshot_instance['id'])
snapshot_instance_get.assert_called_once_with(
utils.IsAMatcher(context.RequestContext),
self.snapshot_instance['id'], with_share_data=True)
snap_get_all_for_snap_instance.assert_called_once_with(
utils.IsAMatcher(context.RequestContext),
self.snapshot_instance['id'])
if state == constants.ACCESS_STATE_QUEUED_TO_APPLY:
self.driver.snapshot_update_access.assert_called_once_with(
utils.IsAMatcher(context.RequestContext),
self.snapshot_instance, rules, add_rules=rules,
delete_rules=[], share_server=None)
else:
self.driver.snapshot_update_access.assert_called_once_with(
utils.IsAMatcher(context.RequestContext),
self.snapshot_instance, [], add_rules=[],
delete_rules=rules, share_server=None)
def test_update_access_rules_delete_all_rules(self):
rules = []
for i in range(2):
rules.append({
'id': 'id-%s' % i,
'state': constants.ACCESS_STATE_QUEUED_TO_DENY,
'access_id': 'rule_id%s' % i
})
snapshot_instance_get = self.mock_object(
db, 'share_snapshot_instance_get',
mock.Mock(return_value=self.snapshot_instance))
snap_get_all_for_snap_instance = self.mock_object(
db, 'share_snapshot_access_get_all_for_snapshot_instance',
mock.Mock(side_effect=[rules, []]))
self.mock_object(db, 'share_snapshot_instance_access_update')
self.mock_object(self.driver, 'snapshot_update_access')
self.mock_object(db, 'share_snapshot_instance_access_delete')
self.snapshot_access.update_access_rules(self.context,
self.snapshot_instance['id'],
delete_all_rules=True)
snapshot_instance_get.assert_called_once_with(
utils.IsAMatcher(context.RequestContext),
self.snapshot_instance['id'], with_share_data=True)
snap_get_all_for_snap_instance.assert_called_with(
utils.IsAMatcher(context.RequestContext),
self.snapshot_instance['id'])
self.driver.snapshot_update_access.assert_called_with(
utils.IsAMatcher(context.RequestContext), self.snapshot_instance,
[], add_rules=[], delete_rules=rules, share_server=None)
def test_update_access_rules_exception(self):
rules = []
for i in range(2):
rules.append({
'id': 'id-%s' % i,
'state': constants.ACCESS_STATE_APPLYING,
'access_id': 'rule_id%s' % i
})
snapshot_instance_get = self.mock_object(
db, 'share_snapshot_instance_get',
mock.Mock(return_value=self.snapshot_instance))
snap_get_all_for_snap_instance = self.mock_object(
db, 'share_snapshot_access_get_all_for_snapshot_instance',
mock.Mock(return_value=rules))
self.mock_object(db, 'share_snapshot_instance_access_update')
self.mock_object(self.driver, 'snapshot_update_access',
mock.Mock(side_effect=exception.NotFound))
self.assertRaises(exception.NotFound,
self.snapshot_access.update_access_rules,
self.context, self.snapshot_instance['id'])
snapshot_instance_get.assert_called_once_with(
utils.IsAMatcher(context.RequestContext),
self.snapshot_instance['id'], with_share_data=True)
snap_get_all_for_snap_instance.assert_called_once_with(
utils.IsAMatcher(context.RequestContext),
self.snapshot_instance['id'])
self.driver.snapshot_update_access.assert_called_once_with(
utils.IsAMatcher(context.RequestContext), self.snapshot_instance,
rules, add_rules=rules, delete_rules=[], share_server=None)

View File

@ -125,6 +125,16 @@ class ManilaExceptionTestCase(test.TestCase):
self.assertEqual(500, e.code)
self.assertIn(reason, e.msg)
def test_snapshot_access_already_exists(self):
# Verify response code for exception.ShareSnapshotAccessExists
access_type = "fake_type"
access = "fake_access"
e = exception.ShareSnapshotAccessExists(access_type=access_type,
access=access)
self.assertEqual(400, e.code)
self.assertIn(access_type, e.msg)
self.assertIn(access, e.msg)
class ManilaExceptionResponseCode400(test.TestCase):
@ -241,6 +251,13 @@ class ManilaExceptionResponseCode400(test.TestCase):
self.assertEqual(400, e.code)
self.assertIn(reason, e.msg)
def test_invalid_share_snapshot_instance(self):
# Verify response code for exception.InvalidShareSnapshotInstance
reason = "fake_reason"
e = exception.InvalidShareSnapshotInstance(reason=reason)
self.assertEqual(400, e.code)
self.assertIn(reason, e.msg)
class ManilaExceptionResponseCode403(test.TestCase):

View File

@ -30,7 +30,7 @@ ShareGroup = [
help="The minimum api microversion is configured to be the "
"value of the minimum microversion supported by Manila."),
cfg.StrOpt("max_api_microversion",
default="2.31",
default="2.32",
help="The maximum api microversion is configured to be the "
"value of the latest microversion supported by Manila."),
cfg.StrOpt("region",
@ -203,6 +203,9 @@ ShareGroup = [
help="Defines whether to run manage/unmanage snapshot tests "
"or not. These tests may leave orphaned resources, so be "
"careful enabling this opt."),
cfg.BoolOpt("run_mount_snapshot_tests",
default=False,
help="Enable or disable mountable snapshot tests."),
cfg.StrOpt("image_with_share_tools",
default="manila-service-image-master",

View File

@ -674,6 +674,26 @@ class SharesV2Client(shares_client.SharesClient):
})
raise exceptions.TimeoutException(message)
def get_snapshot_instance_export_location(
self, instance_id, export_location_uuid,
version=LATEST_MICROVERSION):
resp, body = self.get(
"snapshot-instances/%(instance_id)s/export-locations/%("
"el_uuid)s" % {
"instance_id": instance_id,
"el_uuid": export_location_uuid},
version=version)
self.expected_success(200, resp.status)
return self._parse_resp(body)
def list_snapshot_instance_export_locations(
self, instance_id, version=LATEST_MICROVERSION):
resp, body = self.get(
"snapshot-instances/%s/export-locations" % instance_id,
version=version)
self.expected_success(200, resp.status)
return self._parse_resp(body)
###############
def _get_access_action_name(self, version, action):
@ -1379,3 +1399,102 @@ class SharesV2Client(shares_client.SharesClient):
version=version)
self.expected_success(200, resp.status)
return self._parse_resp(body)
################
def create_snapshot_access_rule(self, snapshot_id, access_type="ip",
access_to="0.0.0.0/0"):
body = {
"allow_access": {
"access_type": access_type,
"access_to": access_to
}
}
resp, body = self.post("snapshots/%s/action" % snapshot_id,
json.dumps(body), version=LATEST_MICROVERSION)
self.expected_success(202, resp.status)
return self._parse_resp(body)
def get_snapshot_access_rule(self, snapshot_id, rule_id):
resp, body = self.get("snapshots/%s/access-list" % snapshot_id,
version=LATEST_MICROVERSION)
body = self._parse_resp(body)
found_rules = filter(lambda x: x['id'] == rule_id, body)
return found_rules[0] if len(found_rules) > 0 else None
def wait_for_snapshot_access_rule_status(self, snapshot_id, rule_id,
expected_state='active'):
rule = self.get_snapshot_access_rule(snapshot_id, rule_id)
state = rule['state']
start = int(time.time())
while state != expected_state:
time.sleep(self.build_interval)
rule = self.get_snapshot_access_rule(snapshot_id, rule_id)
state = rule['state']
if state == expected_state:
return
if 'error' in state:
raise share_exceptions.AccessRuleBuildErrorException(
snapshot_id)
if int(time.time()) - start >= self.build_timeout:
message = ('The status of snapshot access rule %(id)s failed '
'to reach %(expected_state)s state within the '
'required time (%(time)ss). Current '
'state: %(current_state)s.' %
{
'expected_state': expected_state,
'time': self.build_timeout,
'id': rule_id,
'current_state': state,
})
raise exceptions.TimeoutException(message)
def delete_snapshot_access_rule(self, snapshot_id, rule_id):
body = {
"deny_access": {
"access_id": rule_id,
}
}
resp, body = self.post("snapshots/%s/action" % snapshot_id,
json.dumps(body), version=LATEST_MICROVERSION)
self.expected_success(202, resp.status)
return self._parse_resp(body)
def wait_for_snapshot_access_rule_deletion(self, snapshot_id, rule_id):
rule = self.get_snapshot_access_rule(snapshot_id, rule_id)
start = int(time.time())
while rule is not None:
time.sleep(self.build_interval)
rule = self.get_snapshot_access_rule(snapshot_id, rule_id)
if rule is None:
return
if int(time.time()) - start >= self.build_timeout:
message = ('The snapshot access rule %(id)s failed to delete '
'within the required time (%(time)ss).' %
{
'time': self.build_timeout,
'id': rule_id,
})
raise exceptions.TimeoutException(message)
def get_snapshot_export_location(self, snapshot_id, export_location_uuid,
version=LATEST_MICROVERSION):
resp, body = self.get(
"snapshots/%(snapshot_id)s/export-locations/%(el_uuid)s" % {
"snapshot_id": snapshot_id, "el_uuid": export_location_uuid},
version=version)
self.expected_success(200, resp.status)
return self._parse_resp(body)
def list_snapshot_export_locations(
self, snapshot_id, version=LATEST_MICROVERSION):
resp, body = self.get(
"snapshots/%s/export-locations" % snapshot_id, version=version)
self.expected_success(200, resp.status)
return self._parse_resp(body)

View File

@ -80,6 +80,8 @@ class ExtraSpecsAdminNegativeTest(base.BaseSharesMixedTest):
if utils.is_microversion_ge(CONF.share.max_api_microversion,
constants.REVERT_TO_SNAPSHOT_MICROVERSION):
expected_keys.append('revert_to_snapshot_support')
if utils.is_microversion_ge(CONF.share.max_api_microversion, '2.32'):
expected_keys.append('mount_snapshot_support')
actual_keys = share_type['share_type']['extra_specs'].keys()
self.assertEqual(sorted(expected_keys), sorted(actual_keys),
'Incorrect extra specs visible to non-admin user; '

View File

@ -0,0 +1,140 @@
# Copyright (c) 2017 Hitachi Data Systems, Inc.
# 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
from oslo_utils import uuidutils
import six
from tempest import config
import testtools
from testtools import testcase as tc
from manila_tempest_tests.tests.api import base
CONF = config.CONF
LATEST_MICROVERSION = CONF.share.max_api_microversion
@base.skip_if_microversion_lt("2.32")
@testtools.skipUnless(CONF.share.run_mount_snapshot_tests and
CONF.share.run_snapshot_tests,
"Mountable snapshots tests are disabled.")
@ddt.ddt
class SnapshotExportLocationsTest(base.BaseSharesMixedTest):
@classmethod
def setup_clients(cls):
super(SnapshotExportLocationsTest, cls).setup_clients()
cls.admin_client = cls.admin_shares_v2_client
@classmethod
def resource_setup(cls):
super(SnapshotExportLocationsTest, cls).resource_setup()
cls.share = cls.create_share(client=cls.admin_client)
cls.snapshot = cls.create_snapshot_wait_for_active(
cls.share['id'], client=cls.admin_client)
cls.snapshot = cls.admin_client.get_snapshot(cls.snapshot['id'])
cls.snapshot_instances = cls.admin_client.list_snapshot_instances(
snapshot_id=cls.snapshot['id'])
def _verify_export_location_structure(
self, export_locations, role='admin', detail=False):
# Determine which keys to expect based on role, version and format
summary_keys = ['id', 'path', 'links']
if detail:
summary_keys.extend(['created_at', 'updated_at'])
admin_summary_keys = summary_keys + [
'share_snapshot_instance_id', 'is_admin_only']
if role == 'admin':
expected_keys = admin_summary_keys
else:
expected_keys = summary_keys
if not isinstance(export_locations, (list, tuple, set)):
export_locations = (export_locations, )
for export_location in export_locations:
# Check that the correct keys are present
self.assertEqual(len(expected_keys), len(export_location))
for key in expected_keys:
self.assertIn(key, export_location)
# Check the format of ever-present summary keys
self.assertTrue(uuidutils.is_uuid_like(export_location['id']))
self.assertIsInstance(export_location['path'],
six.string_types)
if role == 'admin':
self.assertIn(export_location['is_admin_only'], (True, False))
self.assertTrue(uuidutils.is_uuid_like(
export_location['share_snapshot_instance_id']))
@tc.attr(base.TAG_POSITIVE, base.TAG_API_WITH_BACKEND)
def test_list_snapshot_export_location(self):
export_locations = (
self.admin_client.list_snapshot_export_locations(
self.snapshot['id']))
for el in export_locations:
self._verify_export_location_structure(el)
@tc.attr(base.TAG_POSITIVE, base.TAG_API_WITH_BACKEND)
def test_get_snapshot_export_location(self):
export_locations = (
self.admin_client.list_snapshot_export_locations(
self.snapshot['id']))
for export_location in export_locations:
el = self.admin_client.get_snapshot_export_location(
self.snapshot['id'], export_location['id'])
self._verify_export_location_structure(el, detail=True)
@tc.attr(base.TAG_POSITIVE, base.TAG_API_WITH_BACKEND)
def test_get_snapshot_instance_export_location(self):
for snapshot_instance in self.snapshot_instances:
export_locations = (
self.admin_client.list_snapshot_instance_export_locations(
snapshot_instance['id']))
for el in export_locations:
el = self.admin_client.get_snapshot_instance_export_location(
snapshot_instance['id'], el['id'])
self._verify_export_location_structure(el, detail=True)
@tc.attr(base.TAG_POSITIVE, base.TAG_API_WITH_BACKEND)
def test_snapshot_contains_all_export_locations_of_all_snapshot_instances(
self):
snapshot_export_locations = (
self.admin_client.list_snapshot_export_locations(
self.snapshot['id']))
snapshot_instances_export_locations = []
for snapshot_instance in self.snapshot_instances:
snapshot_instance_export_locations = (
self.admin_client.list_snapshot_instance_export_locations(
snapshot_instance['id']))
snapshot_instances_export_locations.extend(
snapshot_instance_export_locations)
self.assertEqual(
len(snapshot_export_locations),
len(snapshot_instances_export_locations)
)
self.assertEqual(
sorted(snapshot_export_locations, key=lambda el: el['id']),
sorted(snapshot_instances_export_locations,
key=lambda el: el['id'])
)

View File

@ -0,0 +1,140 @@
# Copyright (c) 2017 Hitachi Data Systems, Inc.
# 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.
from tempest import config
from tempest.lib import exceptions as lib_exc
import testtools
from testtools import testcase as tc
from manila_tempest_tests.tests.api import base
CONF = config.CONF
@base.skip_if_microversion_lt("2.32")
@testtools.skipUnless(CONF.share.run_mount_snapshot_tests and
CONF.share.run_snapshot_tests,
"Mountable snapshots tests are disabled.")
class SnapshotExportLocationsNegativeTest(base.BaseSharesMixedTest):
@classmethod
def setup_clients(cls):
super(SnapshotExportLocationsNegativeTest, cls).setup_clients()
cls.admin_client = cls.admin_shares_v2_client
cls.isolated_client = cls.alt_shares_v2_client
@classmethod
def resource_setup(cls):
super(SnapshotExportLocationsNegativeTest, cls).resource_setup()
cls.share = cls.create_share(client=cls.admin_client)
cls.snapshot = cls.create_snapshot_wait_for_active(
cls.share['id'], client=cls.admin_client)
cls.snapshot = cls.admin_client.get_snapshot(cls.snapshot['id'])
cls.snapshot_instances = cls.admin_client.list_snapshot_instances(
snapshot_id=cls.snapshot['id'])
@tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND)
def test_get_inexistent_snapshot_export_location(self):
self.assertRaises(
lib_exc.NotFound,
self.admin_client.get_snapshot_export_location,
self.snapshot['id'],
"fake-inexistent-snapshot-export-location-id",
)
@tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND)
def test_list_snapshot_export_locations_by_member(self):
self.assertRaises(
lib_exc.NotFound,
self.isolated_client.list_snapshot_export_locations,
self.snapshot['id']
)
@tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND)
def test_get_snapshot_export_location_by_member(self):
export_locations = (
self.admin_client.list_snapshot_export_locations(
self.snapshot['id']))
for export_location in export_locations:
if export_location['is_admin_only']:
continue
self.assertRaises(
lib_exc.NotFound,
self.isolated_client.get_snapshot_export_location,
self.snapshot['id'],
export_location['id']
)
@tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND)
def test_get_inexistent_snapshot_instance_export_location(self):
for snapshot_instance in self.snapshot_instances:
self.assertRaises(
lib_exc.NotFound,
self.admin_client.get_snapshot_instance_export_location,
snapshot_instance['id'],
"fake-inexistent-snapshot-export-location-id",
)
@tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND)
def test_get_snapshot_instance_export_location_by_member(self):
for snapshot_instance in self.snapshot_instances:
export_locations = (
self.admin_client.list_snapshot_instance_export_locations(
snapshot_instance['id']))
for el in export_locations:
self.assertRaises(
lib_exc.Forbidden,
self.isolated_client.get_snapshot_instance_export_location,
snapshot_instance['id'], el['id'],
)
@testtools.skipUnless(CONF.share.run_mount_snapshot_tests and
CONF.share.run_snapshot_tests,
"Mountable snapshots tests are disabled.")
@base.skip_if_microversion_lt("2.32")
class SnapshotExportLocationsAPIOnlyNegativeTest(base.BaseSharesMixedTest):
@classmethod
def setup_clients(cls):
super(SnapshotExportLocationsAPIOnlyNegativeTest, cls).setup_clients()
cls.admin_client = cls.admin_shares_v2_client
cls.isolated_client = cls.alt_shares_v2_client
@tc.attr(base.TAG_NEGATIVE, base.TAG_API)
def test_list_export_locations_by_nonexistent_snapshot(self):
self.assertRaises(
lib_exc.NotFound,
self.admin_client.list_snapshot_export_locations,
"fake-inexistent-snapshot-id",
)
@tc.attr(base.TAG_NEGATIVE, base.TAG_API)
def test_list_export_locations_by_nonexistent_snapshot_instance(self):
self.assertRaises(
lib_exc.NotFound,
self.admin_client.list_snapshot_instance_export_locations,
"fake-inexistent-snapshot-instance-id",
)
@tc.attr(base.TAG_NEGATIVE, base.TAG_API)
def test_list_inexistent_snapshot_instance_export_locations_by_member(
self):
self.assertRaises(
lib_exc.Forbidden,
self.isolated_client.list_snapshot_instance_export_locations,
"fake-inexistent-snapshot-instance-id"
)

View File

@ -0,0 +1,101 @@
# Copyright 2016 Hitachi Data Systems
# 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
import ddt
from tempest import config
import testtools
from testtools import testcase as tc
from manila_tempest_tests.tests.api import base
CONF = config.CONF
class BaseShareSnapshotRulesTest(base.BaseSharesTest):
protocol = ""
@classmethod
def resource_setup(cls):
super(BaseShareSnapshotRulesTest, cls).resource_setup()
cls.share = cls.create_share(cls.protocol)
cls.snapshot = cls.create_snapshot_wait_for_active(cls.share['id'])
def _test_create_delete_access_rules(self, access_to):
# create rule
rule = self.shares_v2_client.create_snapshot_access_rule(
self.snapshot['id'], self.access_type, access_to)
for key in ('deleted', 'deleted_at', 'instance_mappings'):
self.assertNotIn(key, list(six.iterkeys(rule)))
self.shares_v2_client.wait_for_snapshot_access_rule_status(
self.snapshot['id'], rule['id'])
# delete rule and wait for deletion
self.shares_v2_client.delete_snapshot_access_rule(self.snapshot['id'],
rule['id'])
self.shares_v2_client.wait_for_snapshot_access_rule_deletion(
self.snapshot['id'], rule['id'])
@base.skip_if_microversion_lt("2.32")
@testtools.skipUnless(CONF.share.run_mount_snapshot_tests and
CONF.share.run_snapshot_tests,
'Mountable snapshots tests are disabled.')
@ddt.ddt
class ShareSnapshotIpRulesForNFSTest(BaseShareSnapshotRulesTest):
protocol = "nfs"
@classmethod
def resource_setup(cls):
if not (cls.protocol in CONF.share.enable_protocols and
cls.protocol in CONF.share.enable_ip_rules_for_protocols):
msg = "IP rule tests for %s protocol are disabled." % cls.protocol
raise cls.skipException(msg)
super(ShareSnapshotIpRulesForNFSTest, cls).resource_setup()
cls.access_type = "ip"
@tc.attr(base.TAG_POSITIVE, base.TAG_BACKEND)
@ddt.data("1.1.1.1", "1.2.3.4/32")
def test_create_delete_access_rules(self, access_to):
self._test_create_delete_access_rules(access_to)
@base.skip_if_microversion_lt("2.32")
@testtools.skipUnless(CONF.share.run_mount_snapshot_tests,
'Mountable snapshots tests are disabled.')
@ddt.ddt
class ShareSnapshotUserRulesForCIFSTest(BaseShareSnapshotRulesTest):
protocol = "cifs"
@classmethod
def resource_setup(cls):
if not (cls.protocol in CONF.share.enable_protocols and
cls.protocol in CONF.share.enable_user_rules_for_protocols):
msg = ("User rule tests for %s protocol are "
"disabled." % cls.protocol)
raise cls.skipException(msg)
super(ShareSnapshotUserRulesForCIFSTest, cls).resource_setup()
cls.access_type = "user"
@tc.attr(base.TAG_POSITIVE, base.TAG_BACKEND)
def test_create_delete_access_rules(self):
access_to = CONF.share.username_for_user_rules
self._test_create_delete_access_rules(access_to)

View File

@ -0,0 +1,90 @@
# Copyright 2016 Hitachi Data Systems
# 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
from tempest import config
from tempest.lib import exceptions as lib_exc
import testtools
from testtools import testcase as tc
from manila_tempest_tests.tests.api import base
from manila_tempest_tests.tests.api import test_snapshot_rules
CONF = config.CONF
@base.skip_if_microversion_lt("2.32")
@testtools.skipUnless(CONF.share.run_mount_snapshot_tests and
CONF.share.run_snapshot_tests,
'Mountable snapshots tests are disabled.')
@ddt.ddt
class SnapshotIpRulesForNFSNegativeTest(
test_snapshot_rules.BaseShareSnapshotRulesTest):
protocol = "nfs"
@classmethod
def resource_setup(cls):
if not (cls.protocol in CONF.share.enable_protocols and
cls.protocol in CONF.share.enable_ip_rules_for_protocols):
msg = "IP rule tests for %s protocol are disabled." % cls.protocol
raise cls.skipException(msg)
super(SnapshotIpRulesForNFSNegativeTest, cls).resource_setup()
# create share
cls.share = cls.create_share(cls.protocol)
cls.snap = cls.create_snapshot_wait_for_active(cls.share["id"])
@tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND)
@ddt.data("1.2.3.256", "1.1.1.-", "1.2.3.4/33", "1.2.3.*", "1.2.3.*/23",
"1.2.3.1|23", "1.2.3.1/", "1.2.3.1/-1", "fe00::1",
"fe80::217:f2ff:fe07:ed62", "2001:db8::/48", "::1/128",
"ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff",
"2001:0db8:0000:85a3:0000:0000:ac1f:8001")
def test_create_access_rule_ip_with_wrong_target(self, target):
self.assertRaises(lib_exc.BadRequest,
self.shares_v2_client.create_snapshot_access_rule,
self.snap["id"], "ip", target)
@tc.attr(base.TAG_NEGATIVE, base.TAG_API_WITH_BACKEND)
def test_create_duplicate_of_ip_rule(self):
self._test_duplicate_rules()
self._test_duplicate_rules()
def _test_duplicate_rules(self):
# test data
access_type = "ip"
access_to = "1.2.3.4"
# create rule
rule = self.shares_v2_client.create_snapshot_access_rule(
self.snap['id'], access_type, access_to)
self.shares_v2_client.wait_for_snapshot_access_rule_status(
self.snap['id'], rule['id'])
# try create duplicate of rule
self.assertRaises(lib_exc.BadRequest,
self.shares_v2_client.create_snapshot_access_rule,
self.snap["id"], access_type, access_to)
# delete rule and wait for deletion
self.shares_v2_client.delete_snapshot_access_rule(self.snap['id'],
rule['id'])
self.shares_v2_client.wait_for_snapshot_access_rule_deletion(
self.snap['id'], rule['id'])
self.assertRaises(lib_exc.NotFound,
self.shares_v2_client.delete_snapshot_access_rule,
self.snap['id'], rule['id'])

View File

@ -0,0 +1,7 @@
---
features:
- Added mountable snapshots feature to manila. Access can now
be allowed and denied to snapshots of shares created
with a share type that supports this feature.
- Added mountable snapshots support to the LVM driver.