From 8d71932c6902725f80ab75bf11a37d6f673ad2b0 Mon Sep 17 00:00:00 2001 From: tpsilva Date: Fri, 8 Jul 2016 14:41:35 -0300 Subject: [PATCH] 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 Co-Authored-By: Alyson Rosa Co-Authored-By: Miriam Yumi Partially-implements: blueprint manila-mountable-snapshots Change-Id: I65f398a05f82eef31ec317d70dfa101483b44b30 --- contrib/ci/post_test_hook.sh | 6 + contrib/ci/pre_test_hook.sh | 4 +- etc/manila/policy.json | 7 + manila/api/common.py | 96 ++++++ manila/api/openstack/api_version_request.py | 3 +- .../openstack/rest_api_version_history.rst | 4 + manila/api/v1/shares.py | 92 +----- manila/api/v2/router.py | 45 +++ .../api/v2/share_snapshot_export_locations.py | 65 ++++ ...hare_snapshot_instance_export_locations.py | 70 +++++ manila/api/v2/share_snapshots.py | 114 ++++++- .../views/share_snapshot_export_locations.py | 61 ++++ manila/api/views/share_snapshots.py | 18 ++ manila/api/views/shares.py | 6 + manila/common/constants.py | 15 + manila/db/api.py | 75 +++++ .../a77e2ad5012d_add_share_snapshot_access.py | 101 +++++++ manila/db/sqlalchemy/api.py | 286 ++++++++++++++++++ manila/db/sqlalchemy/models.py | 138 ++++++++- manila/exception.py | 8 + manila/scheduler/host_manager.py | 6 + manila/scheduler/utils.py | 1 + manila/share/api.py | 92 +++++- manila/share/driver.py | 39 ++- manila/share/drivers/lvm.py | 71 +++++ manila/share/manager.py | 96 +++++- manila/share/rpcapi.py | 10 +- manila/share/share_types.py | 2 + manila/share/snapshot_access.py | 167 ++++++++++ manila/tests/api/contrib/stubs.py | 1 + manila/tests/api/test_common.py | 34 +++ manila/tests/api/v1/test_shares.py | 14 - .../test_share_snapshot_export_locations.py | 116 +++++++ ...hare_snapshot_instance_export_locations.py | 113 +++++++ manila/tests/api/v2/test_share_snapshots.py | 229 ++++++++++++++ manila/tests/api/v2/test_share_types.py | 1 + manila/tests/db/sqlalchemy/test_api.py | 133 ++++++++ manila/tests/db_utils.py | 21 ++ manila/tests/fake_share.py | 1 + manila/tests/scheduler/fakes.py | 3 + manila/tests/scheduler/test_host_manager.py | 11 + .../share/drivers/dell_emc/test_driver.py | 1 + manila/tests/share/drivers/dummy.py | 20 +- .../glusterfs/test_glusterfs_native.py | 1 + .../share/drivers/hpe/test_hpe_3par_driver.py | 3 + .../share/drivers/huawei/test_huawei_nas.py | 1 + manila/tests/share/drivers/test_lvm.py | 62 +++- .../share/drivers/zfsonlinux/test_driver.py | 1 + manila/tests/share/test_api.py | 173 +++++++++++ manila/tests/share/test_driver.py | 9 +- manila/tests/share/test_manager.py | 95 +++++- manila/tests/share/test_rpcapi.py | 12 + manila/tests/share/test_share_types.py | 3 +- manila/tests/share/test_snapshot_access.py | 161 ++++++++++ manila/tests/test_exception.py | 17 ++ manila_tempest_tests/config.py | 5 +- .../services/share/v2/json/shares_client.py | 119 ++++++++ .../test_share_types_extra_specs_negative.py | 2 + .../admin/test_snapshot_export_locations.py | 140 +++++++++ ...test_snapshot_export_locations_negative.py | 140 +++++++++ .../tests/api/test_snapshot_rules.py | 101 +++++++ .../tests/api/test_snapshot_rules_negative.py | 90 ++++++ ...hare-mount-snapshots-b52bf3433d1e7afb.yaml | 7 + 63 files changed, 3390 insertions(+), 148 deletions(-) create mode 100644 manila/api/v2/share_snapshot_export_locations.py create mode 100644 manila/api/v2/share_snapshot_instance_export_locations.py create mode 100644 manila/api/views/share_snapshot_export_locations.py create mode 100644 manila/db/migrations/alembic/versions/a77e2ad5012d_add_share_snapshot_access.py create mode 100644 manila/share/snapshot_access.py create mode 100644 manila/tests/api/v2/test_share_snapshot_export_locations.py create mode 100644 manila/tests/api/v2/test_share_snapshot_instance_export_locations.py create mode 100644 manila/tests/share/test_snapshot_access.py create mode 100644 manila_tempest_tests/tests/api/admin/test_snapshot_export_locations.py create mode 100644 manila_tempest_tests/tests/api/admin/test_snapshot_export_locations_negative.py create mode 100644 manila_tempest_tests/tests/api/test_snapshot_rules.py create mode 100644 manila_tempest_tests/tests/api/test_snapshot_rules_negative.py create mode 100644 releasenotes/notes/share-mount-snapshots-b52bf3433d1e7afb.yaml diff --git a/contrib/ci/post_test_hook.sh b/contrib/ci/post_test_hook.sh index d66ea8db89..e34701411e 100755 --- a/contrib/ci/post_test_hook.sh +++ b/contrib/ci/post_test_hook.sh @@ -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 diff --git a/contrib/ci/pre_test_hook.sh b/contrib/ci/pre_test_hook.sh index bb1a8112c6..be011be772 100755 --- a/contrib/ci/pre_test_hook.sh +++ b/contrib/ci/pre_test_hook.sh @@ -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 diff --git a/etc/manila/policy.json b/etc/manila/policy.json index 7d964fa024..7c95202ed6 100644 --- a/etc/manila/policy.json +++ b/etc/manila/policy.json @@ -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", diff --git a/manila/api/common.py b/manila/api/common.py index 5f748ae468..385c92147a 100644 --- a/manila/api/common.py +++ b/manila/api/common.py @@ -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) diff --git a/manila/api/openstack/api_version_request.py b/manila/api/openstack/api_version_request.py index cfb467d17d..da2368da56 100644 --- a/manila/api/openstack/api_version_request.py +++ b/manila/api/openstack/api_version_request.py @@ -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 diff --git a/manila/api/openstack/rest_api_version_history.rst b/manila/api/openstack/rest_api_version_history.rst index 3747cc4aad..cecc5aa582 100644 --- a/manila/api/openstack/rest_api_version_history.rst +++ b/manila/api/openstack/rest_api_version_history.rst @@ -187,3 +187,7 @@ user documentation. 2.31 ---- Convert consistency groups to share groups. + +2.32 +---- + Added mountable snapshots APIs. diff --git a/manila/api/v1/shares.py b/manila/api/v1/shares.py index 944ebe3f7d..1833410d19 100644 --- a/manila/api/v1/shares.py +++ b/manila/api/v1/shares.py @@ -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, diff --git a/manila/api/v2/router.py b/manila/api/v2/router.py index 8740d34d15..462b2143af 100644 --- a/manila/api/v2/router.py +++ b/manila/api/v2/router.py @@ -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"] diff --git a/manila/api/v2/share_snapshot_export_locations.py b/manila/api/v2/share_snapshot_export_locations.py new file mode 100644 index 0000000000..056a3bab0d --- /dev/null +++ b/manila/api/v2/share_snapshot_export_locations.py @@ -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()) diff --git a/manila/api/v2/share_snapshot_instance_export_locations.py b/manila/api/v2/share_snapshot_instance_export_locations.py new file mode 100644 index 0000000000..3a5ec7242a --- /dev/null +++ b/manila/api/v2/share_snapshot_instance_export_locations.py @@ -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()) diff --git a/manila/api/v2/share_snapshots.py b/manila/api/v2/share_snapshots.py index 8775b868d8..257469020c 100644 --- a/manila/api/v2/share_snapshots.py +++ b/manila/api/v2/share_snapshots.py @@ -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()) diff --git a/manila/api/views/share_snapshot_export_locations.py b/manila/api/views/share_snapshot_export_locations.py new file mode 100644 index 0000000000..66f1054c14 --- /dev/null +++ b/manila/api/views/share_snapshot_export_locations.py @@ -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) diff --git a/manila/api/views/share_snapshots.py b/manila/api/views/share_snapshots.py index 6314a0ba8d..d6500a0374 100644 --- a/manila/api/views/share_snapshots.py +++ b/manila/api/views/share_snapshots.py @@ -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]) + } diff --git a/manila/api/views/shares.py b/manila/api/views/shares.py index 6a704d77b4..7af27a3b4d 100644 --- a/manila/api/views/shares.py +++ b/manila/api/views/shares.py @@ -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] diff --git a/manila/common/constants.py b/manila/common/constants.py index 53f3a7997f..70e7dc0056 100644 --- a/manila/common/constants.py +++ b/manila/common/constants.py @@ -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') diff --git a/manila/db/api.py b/manila/db/api.py index 810011bfb7..385c88c834 100644 --- a/manila/db/api.py +++ b/manila/db/api.py @@ -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.""" diff --git a/manila/db/migrations/alembic/versions/a77e2ad5012d_add_share_snapshot_access.py b/manila/db/migrations/alembic/versions/a77e2ad5012d_add_share_snapshot_access.py new file mode 100644 index 0000000000..25a2eac318 --- /dev/null +++ b/manila/db/migrations/alembic/versions/a77e2ad5012d_add_share_snapshot_access.py @@ -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') diff --git a/manila/db/sqlalchemy/api.py b/manila/db/sqlalchemy/api.py index 120e824e2e..db7f36ccc0 100644 --- a/manila/db/sqlalchemy/api.py +++ b/manila/db/sqlalchemy/api.py @@ -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) ################################# diff --git a/manila/db/sqlalchemy/models.py b/manila/db/sqlalchemy/models.py index 5a870c6a77..8ebaa4cfbe 100644 --- a/manila/db/sqlalchemy/models.py +++ b/manila/db/sqlalchemy/models.py @@ -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 diff --git a/manila/exception.py b/manila/exception.py index ac2bf27e17..e6c6c2e0ed 100644 --- a/manila/exception.py +++ b/manila/exception.py @@ -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.") diff --git a/manila/scheduler/host_manager.py b/manila/scheduler/host_manager.py index 22b49cbb43..88b62a93b2 100644 --- a/manila/scheduler/host_manager.py +++ b/manila/scheduler/host_manager.py @@ -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') diff --git a/manila/scheduler/utils.py b/manila/scheduler/utils.py index f27d43e158..a73c0f13f4 100644 --- a/manila/scheduler/utils.py +++ b/manila/scheduler/utils.py @@ -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, diff --git a/manila/share/api.py b/manila/share/api.py index 2650a8287f..4a3bea0d39 100644 --- a/manila/share/api.py +++ b/manila/share/api.py @@ -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) diff --git a/manila/share/driver.py b/manila/share/driver.py index 6d1c1e6023..6926119f61 100644 --- a/manila/share/driver.py +++ b/manila/share/driver.py @@ -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() diff --git a/manila/share/drivers/lvm.py b/manila/share/drivers/lvm.py index 12745aee71..0e450b27e2 100644 --- a/manila/share/drivers/lvm.py +++ b/manila/share/drivers/lvm.py @@ -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 diff --git a/manila/share/manager.py b/manila/share/manager.py index cc54ff21b4..3b71df3bfe 100644 --- a/manila/share/manager.py +++ b/manila/share/manager.py @@ -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) diff --git a/manila/share/rpcapi.py b/manila/share/rpcapi.py index 848edb0af3..947f922f63 100644 --- a/manila/share/rpcapi.py +++ b/manila/share/rpcapi.py @@ -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']) diff --git a/manila/share/share_types.py b/manila/share/share_types.py index 3489c65ad5..137502e2ed 100644 --- a/manila/share/share_types.py +++ b/manila/share/share_types.py @@ -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 diff --git a/manila/share/snapshot_access.py b/manila/share/snapshot_access.py new file mode 100644 index 0000000000..2e86056cd2 --- /dev/null +++ b/manila/share/snapshot_access.py @@ -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) diff --git a/manila/tests/api/contrib/stubs.py b/manila/tests/api/contrib/stubs.py index fe53e4aad2..1c517d91a9 100644 --- a/manila/tests/api/contrib/stubs.py +++ b/manila/tests/api/contrib/stubs.py @@ -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, } diff --git a/manila/tests/api/test_common.py b/manila/tests/api/test_common.py index 4da0856d9c..7da9d3c5ed 100644 --- a/manila/tests/api/test_common.py +++ b/manila/tests/api/test_common.py @@ -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): diff --git a/manila/tests/api/v1/test_shares.py b/manila/tests/api/v1/test_shares.py index dd086f502c..c91fed40aa 100644 --- a/manila/tests/api/v1/test_shares.py +++ b/manila/tests/api/v1/test_shares.py @@ -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): diff --git a/manila/tests/api/v2/test_share_snapshot_export_locations.py b/manila/tests/api/v2/test_share_snapshot_export_locations.py new file mode 100644 index 0000000000..f9dc81dcf5 --- /dev/null +++ b/manila/tests/api/v2/test_share_snapshot_export_locations.py @@ -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'] + ) diff --git a/manila/tests/api/v2/test_share_snapshot_instance_export_locations.py b/manila/tests/api/v2/test_share_snapshot_instance_export_locations.py new file mode 100644 index 0000000000..e7a3958a58 --- /dev/null +++ b/manila/tests/api/v2/test_share_snapshot_instance_export_locations.py @@ -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'] + ) diff --git a/manila/tests/api/v2/test_share_snapshots.py b/manila/tests/api/v2/test_share_snapshots.py index 1707c7c35f..c18730e833 100644 --- a/manila/tests/api/v2/test_share_snapshots.py +++ b/manila/tests/api/v2/test_share_snapshots.py @@ -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): diff --git a/manila/tests/api/v2/test_share_types.py b/manila/tests/api/v2/test_share_types.py index 9ea3e61c36..455e73d363 100644 --- a/manila/tests/api/v2/test_share_types.py +++ b/manila/tests/api/v2/test_share_types.py @@ -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() diff --git a/manila/tests/db/sqlalchemy/test_api.py b/manila/tests/db/sqlalchemy/test_api.py index 0b24850a6d..d989794b18 100644 --- a/manila/tests/db/sqlalchemy/test_api.py +++ b/manila/tests/db/sqlalchemy/test_api.py @@ -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): diff --git a/manila/tests/db_utils.py b/manila/tests/db_utils.py index 043513b678..81e7b84d75 100644 --- a/manila/tests/db_utils.py +++ b/manila/tests/db_utils.py @@ -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', {}) diff --git a/manila/tests/fake_share.py b/manila/tests/fake_share.py index 14ffdea802..ad7d122ab1 100644 --- a/manila/tests/fake_share.py +++ b/manila/tests/fake_share.py @@ -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) diff --git a/manila/tests/scheduler/fakes.py b/manila/tests/scheduler/fakes.py index 64a8de36dc..ec861e6ce5 100644 --- a/manila/tests/scheduler/fakes.py +++ b/manila/tests/scheduler/fakes.py @@ -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), } diff --git a/manila/tests/scheduler/test_host_manager.py b/manila/tests/scheduler/test_host_manager.py index 593a7199ff..67e5901f74 100644 --- a/manila/tests/scheduler/test_host_manager.py +++ b/manila/tests/scheduler/test_host_manager.py @@ -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, diff --git a/manila/tests/share/drivers/dell_emc/test_driver.py b/manila/tests/share/drivers/dell_emc/test_driver.py index 9d754a0cad..aa5f03a514 100644 --- a/manila/tests/share/drivers/dell_emc/test_driver.py +++ b/manila/tests/share/drivers/dell_emc/test_driver.py @@ -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 diff --git a/manila/tests/share/drivers/dummy.py b/manila/tests/share/drivers/dummy.py index 79312d012d..21b2cab300 100644 --- a/manila/tests/share/drivers/dummy.py +++ b/manila/tests/share/drivers/dummy.py @@ -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(), } diff --git a/manila/tests/share/drivers/glusterfs/test_glusterfs_native.py b/manila/tests/share/drivers/glusterfs/test_glusterfs_native.py index f1b3f10ddd..b86b8fe065 100644 --- a/manila/tests/share/drivers/glusterfs/test_glusterfs_native.py +++ b/manila/tests/share/drivers/glusterfs/test_glusterfs_native.py @@ -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, diff --git a/manila/tests/share/drivers/hpe/test_hpe_3par_driver.py b/manila/tests/share/drivers/hpe/test_hpe_3par_driver.py index d9fc94749c..ddeaaa4aac 100644 --- a/manila/tests/share/drivers/hpe/test_hpe_3par_driver.py +++ b/manila/tests/share/drivers/hpe/test_hpe_3par_driver.py @@ -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, diff --git a/manila/tests/share/drivers/huawei/test_huawei_nas.py b/manila/tests/share/drivers/huawei/test_huawei_nas.py index d0f5c43d9b..a521483c25 100644 --- a/manila/tests/share/drivers/huawei/test_huawei_nas.py +++ b/manila/tests/share/drivers/huawei/test_huawei_nas.py @@ -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, diff --git a/manila/tests/share/drivers/test_lvm.py b/manila/tests/share/drivers/test_lvm.py index 7a7f265c2b..fe8df0f294 100644 --- a/manila/tests/share/drivers/test_lvm.py +++ b/manila/tests/share/drivers/test_lvm.py @@ -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)) diff --git a/manila/tests/share/drivers/zfsonlinux/test_driver.py b/manila/tests/share/drivers/zfsonlinux/test_driver.py index 376aebd0cd..4081cf1192 100644 --- a/manila/tests/share/drivers/zfsonlinux/test_driver.py +++ b/manila/tests/share/drivers/zfsonlinux/test_driver.py @@ -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', diff --git a/manila/tests/share/test_api.py b/manila/tests/share/test_api.py index 50e67e834f..61e0ecfead 100644 --- a/manila/tests/share/test_api.py +++ b/manila/tests/share/test_api.py @@ -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, }, } diff --git a/manila/tests/share/test_driver.py b/manila/tests/share/test_driver.py index 9890d3baa9..7d2284d70a 100644 --- a/manila/tests/share/test_driver.py +++ b/manila/tests/share/test_driver.py @@ -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'], + [], []) diff --git a/manila/tests/share/test_manager.py b/manila/tests/share/test_manager.py index 6c0b99a3ff..76f262c82a 100644 --- a/manila/tests/share/test_manager.py +++ b/manila/tests/share/test_manager.py @@ -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( diff --git a/manila/tests/share/test_rpcapi.py b/manila/tests/share/test_rpcapi.py index 7ae52e8489..206fd0f943 100644 --- a/manila/tests/share/test_rpcapi.py +++ b/manila/tests/share/test_rpcapi.py @@ -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']) diff --git a/manila/tests/share/test_share_types.py b/manila/tests/share/test_share_types.py index 5592fdd124..404fd7c92a 100644 --- a/manila/tests/share/test_share_types.py +++ b/manila/tests/share/test_share_types.py @@ -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,), diff --git a/manila/tests/share/test_snapshot_access.py b/manila/tests/share/test_snapshot_access.py new file mode 100644 index 0000000000..8cfcb45198 --- /dev/null +++ b/manila/tests/share/test_snapshot_access.py @@ -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) diff --git a/manila/tests/test_exception.py b/manila/tests/test_exception.py index 476655bdbb..bc1f9c6f6b 100644 --- a/manila/tests/test_exception.py +++ b/manila/tests/test_exception.py @@ -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): diff --git a/manila_tempest_tests/config.py b/manila_tempest_tests/config.py index d25cb8dd68..e3a8936cc8 100644 --- a/manila_tempest_tests/config.py +++ b/manila_tempest_tests/config.py @@ -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", diff --git a/manila_tempest_tests/services/share/v2/json/shares_client.py b/manila_tempest_tests/services/share/v2/json/shares_client.py index aa176cb452..1190dfe245 100644 --- a/manila_tempest_tests/services/share/v2/json/shares_client.py +++ b/manila_tempest_tests/services/share/v2/json/shares_client.py @@ -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) diff --git a/manila_tempest_tests/tests/api/admin/test_share_types_extra_specs_negative.py b/manila_tempest_tests/tests/api/admin/test_share_types_extra_specs_negative.py index 63fdf233d1..f888969e55 100644 --- a/manila_tempest_tests/tests/api/admin/test_share_types_extra_specs_negative.py +++ b/manila_tempest_tests/tests/api/admin/test_share_types_extra_specs_negative.py @@ -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; ' diff --git a/manila_tempest_tests/tests/api/admin/test_snapshot_export_locations.py b/manila_tempest_tests/tests/api/admin/test_snapshot_export_locations.py new file mode 100644 index 0000000000..dd48df460e --- /dev/null +++ b/manila_tempest_tests/tests/api/admin/test_snapshot_export_locations.py @@ -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']) + ) diff --git a/manila_tempest_tests/tests/api/admin/test_snapshot_export_locations_negative.py b/manila_tempest_tests/tests/api/admin/test_snapshot_export_locations_negative.py new file mode 100644 index 0000000000..6fccc4d121 --- /dev/null +++ b/manila_tempest_tests/tests/api/admin/test_snapshot_export_locations_negative.py @@ -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" + ) diff --git a/manila_tempest_tests/tests/api/test_snapshot_rules.py b/manila_tempest_tests/tests/api/test_snapshot_rules.py new file mode 100644 index 0000000000..af80f4dfb7 --- /dev/null +++ b/manila_tempest_tests/tests/api/test_snapshot_rules.py @@ -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) diff --git a/manila_tempest_tests/tests/api/test_snapshot_rules_negative.py b/manila_tempest_tests/tests/api/test_snapshot_rules_negative.py new file mode 100644 index 0000000000..9f48b735a1 --- /dev/null +++ b/manila_tempest_tests/tests/api/test_snapshot_rules_negative.py @@ -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']) diff --git a/releasenotes/notes/share-mount-snapshots-b52bf3433d1e7afb.yaml b/releasenotes/notes/share-mount-snapshots-b52bf3433d1e7afb.yaml new file mode 100644 index 0000000000..574a744a61 --- /dev/null +++ b/releasenotes/notes/share-mount-snapshots-b52bf3433d1e7afb.yaml @@ -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. +