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:
parent
c6d2ae6492
commit
8d71932c69
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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",
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -187,3 +187,7 @@ user documentation.
|
||||
2.31
|
||||
----
|
||||
Convert consistency groups to share groups.
|
||||
|
||||
2.32
|
||||
----
|
||||
Added mountable snapshots APIs.
|
||||
|
@ -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,
|
||||
|
@ -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"]
|
||||
|
||||
|
65
manila/api/v2/share_snapshot_export_locations.py
Normal file
65
manila/api/v2/share_snapshot_export_locations.py
Normal 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())
|
70
manila/api/v2/share_snapshot_instance_export_locations.py
Normal file
70
manila/api/v2/share_snapshot_instance_export_locations.py
Normal 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())
|
@ -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())
|
||||
|
61
manila/api/views/share_snapshot_export_locations.py
Normal file
61
manila/api/views/share_snapshot_export_locations.py
Normal 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)
|
@ -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])
|
||||
}
|
||||
|
@ -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]
|
||||
|
@ -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')
|
||||
|
@ -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."""
|
||||
|
@ -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')
|
@ -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)
|
||||
|
||||
#################################
|
||||
|
||||
|
@ -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
|
||||
|
@ -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.")
|
||||
|
@ -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')
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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'])
|
||||
|
@ -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
|
||||
|
||||
|
167
manila/share/snapshot_access.py
Normal file
167
manila/share/snapshot_access.py
Normal 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)
|
@ -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,
|
||||
}
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
||||
|
116
manila/tests/api/v2/test_share_snapshot_export_locations.py
Normal file
116
manila/tests/api/v2/test_share_snapshot_export_locations.py
Normal 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']
|
||||
)
|
@ -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']
|
||||
)
|
@ -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):
|
||||
|
@ -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()
|
||||
|
@ -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):
|
||||
|
||||
|
@ -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', {})
|
||||
|
@ -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)
|
||||
|
@ -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),
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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(),
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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))
|
||||
|
@ -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',
|
||||
|
@ -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,
|
||||
},
|
||||
}
|
||||
|
@ -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'],
|
||||
[], [])
|
||||
|
@ -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(
|
||||
|
@ -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'])
|
||||
|
@ -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,),
|
||||
|
161
manila/tests/share/test_snapshot_access.py
Normal file
161
manila/tests/share/test_snapshot_access.py
Normal 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)
|
@ -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):
|
||||
|
||||
|
@ -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",
|
||||
|
@ -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)
|
||||
|
@ -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; '
|
||||
|
@ -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'])
|
||||
)
|
@ -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"
|
||||
)
|
101
manila_tempest_tests/tests/api/test_snapshot_rules.py
Normal file
101
manila_tempest_tests/tests/api/test_snapshot_rules.py
Normal 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)
|
@ -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'])
|
@ -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.
|
||||
|
Loading…
Reference in New Issue
Block a user