# Copyright (c) 2015 Mirantis 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 oslo_log import log import six from manila.common import constants from manila import exception from manila.i18n import _ from manila.i18n import _LI from manila import utils LOG = log.getLogger(__name__) class ShareInstanceAccess(object): def __init__(self, db, driver): self.db = db self.driver = driver def update_access_rules(self, context, share_instance_id, add_rules=None, delete_rules=None, share_server=None): """Update access rules in driver and database for given share instance. :param context: current context :param share_instance_id: Id of the share instance model :param add_rules: list with ShareAccessMapping models or None - rules which should be added :param delete_rules: list with ShareAccessMapping models, "all", None - rules which should be deleted. If "all" is provided - all rules will be deleted. :param share_server: Share server model or None """ share_instance = self.db.share_instance_get( context, share_instance_id, with_share_data=True) share_id = share_instance["share_id"] @utils.synchronized( "update_access_rules_for_share_%s" % share_id, external=True) def _update_access_rules_locked(*args, **kwargs): return self._update_access_rules(*args, **kwargs) _update_access_rules_locked( context=context, share_instance_id=share_instance_id, add_rules=add_rules, delete_rules=delete_rules, share_server=share_server, ) def _update_access_rules(self, context, share_instance_id, add_rules=None, delete_rules=None, share_server=None): # Reget share instance share_instance = self.db.share_instance_get( context, share_instance_id, with_share_data=True) # NOTE (rraja): preserve error state to trigger maintenance mode if share_instance['access_rules_status'] != constants.STATUS_ERROR: self.db.share_instance_update_access_status( context, share_instance_id, constants.STATUS_UPDATING) add_rules = add_rules or [] delete_rules = delete_rules or [] remove_rules = None if six.text_type(delete_rules).lower() == "all": # NOTE(ganso): if we are deleting an instance or clearing all # the rules, we want to remove only the ones related # to this instance. delete_rules = self.db.share_access_get_all_for_instance( context, share_instance['id']) rules = [] else: _rules = self.db.share_access_get_all_for_instance( context, share_instance['id']) rules = _rules if delete_rules: delete_ids = [rule['id'] for rule in delete_rules] rules = list(filter(lambda r: r['id'] not in delete_ids, rules)) # NOTE(ganso): trigger maintenance mode if share_instance['access_rules_status'] == ( constants.STATUS_ERROR): remove_rules = [ rule for rule in _rules if rule["id"] in delete_ids] delete_rules = [] try: access_keys = None try: access_keys = self.driver.update_access( context, share_instance, rules, add_rules=add_rules, delete_rules=delete_rules, share_server=share_server ) except NotImplementedError: # NOTE(u_glide): Fallback to legacy allow_access/deny_access # for drivers without update_access() method support self._update_access_fallback(add_rules, context, delete_rules, remove_rules, share_instance, share_server) if access_keys: self._validate_access_keys(rules, add_rules, delete_rules, access_keys) for access_id, access_key in access_keys.items(): self.db.share_access_update_access_key( context, access_id, access_key) except Exception: self.db.share_instance_update_access_status( context, share_instance['id'], constants.STATUS_ERROR) raise # NOTE(ganso): remove rules after maintenance is complete if remove_rules: delete_rules = remove_rules self._remove_access_rules(context, delete_rules, share_instance['id']) share_instance = self.db.share_instance_get(context, share_instance_id, with_share_data=True) if self._check_needs_refresh(context, rules, share_instance): self._update_access_rules(context, share_instance_id, share_server=share_server) else: self.db.share_instance_update_access_status( context, share_instance['id'], constants.STATUS_ACTIVE ) LOG.info(_LI("Access rules were successfully applied for " "share instance: %s"), share_instance['id']) @staticmethod def _validate_access_keys(access_rules, add_rules, delete_rules, access_keys): if not isinstance(access_keys, dict): msg = _("The access keys must be supplied as a dictionary that " "maps rule IDs to access keys.") raise exception.Invalid(message=msg) actual_rule_ids = sorted(access_keys) expected_rule_ids = [] if not (add_rules or delete_rules): expected_rule_ids = [rule['id'] for rule in access_rules] else: expected_rule_ids = [rule['id'] for rule in add_rules] if actual_rule_ids != sorted(expected_rule_ids): msg = (_("The rule IDs supplied: %(actual)s do not match the " "rule IDs that are expected: %(expected)s.") % {'actual': actual_rule_ids, 'expected': expected_rule_ids}) raise exception.Invalid(message=msg) for access_key in access_keys.values(): if not isinstance(access_key, six.string_types): msg = (_("Access key %s is not string type.") % access_key) raise exception.Invalid(message=msg) def _check_needs_refresh(self, context, rules, share_instance): rule_ids = set([rule['id'] for rule in rules]) queried_rules = self.db.share_access_get_all_for_instance( context, share_instance['id']) queried_ids = set([rule['id'] for rule in queried_rules]) access_rules_status = share_instance['access_rules_status'] return (access_rules_status == constants.STATUS_UPDATING_MULTIPLE or rule_ids != queried_ids) def _update_access_fallback(self, add_rules, context, delete_rules, remove_rules, share_instance, share_server): for rule in add_rules: LOG.info( _LI("Applying access rule '%(rule)s' for share " "instance '%(instance)s'"), {'rule': rule['id'], 'instance': share_instance['id']} ) self.driver.allow_access( context, share_instance, rule, share_server=share_server ) # NOTE(ganso): Fallback mode temporary compatibility workaround if remove_rules: delete_rules = remove_rules for rule in delete_rules: LOG.info( _LI("Denying access rule '%(rule)s' from share " "instance '%(instance)s'"), {'rule': rule['id'], 'instance': share_instance['id']} ) self.driver.deny_access( context, share_instance, rule, share_server=share_server ) def _remove_access_rules(self, context, access_rules, share_instance_id): if not access_rules: return for rule in access_rules: access_mapping = self.db.share_instance_access_get( context, rule['id'], share_instance_id) self.db.share_instance_access_delete(context, access_mapping['id'])