269 lines
11 KiB
Python
269 lines
11 KiB
Python
# 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 _, _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 = []
|
|
|
|
# NOTE(ganso): up to here we are certain of the rules that we are
|
|
# supposed to pass to drivers. 'rules' variable is used for validating
|
|
# the refresh mechanism later, according to the 'supposed' rules.
|
|
driver_rules = rules
|
|
|
|
if share_instance['status'] == constants.STATUS_MIGRATING:
|
|
# NOTE(ganso): If the share instance is the source in a migration,
|
|
# it should have all its rules cast to read-only.
|
|
|
|
readonly_support = self.driver.configuration.safe_get(
|
|
'migration_readonly_rules_support')
|
|
|
|
# NOTE(ganso): If the backend supports read-only rules, then all
|
|
# rules are cast to read-only here and passed to drivers.
|
|
if readonly_support:
|
|
for rule in driver_rules + add_rules:
|
|
rule['access_level'] = constants.ACCESS_LEVEL_RO
|
|
LOG.debug("All access rules of share instance %s are being "
|
|
"cast to read-only for migration.",
|
|
share_instance['id'])
|
|
# NOTE(ganso): If the backend does not support read-only rules, we
|
|
# will remove all access to the share and have only the access
|
|
# requested by the Data Service for migration as RW.
|
|
else:
|
|
LOG.debug("All previously existing access rules of share "
|
|
"instance %s are being removed for migration, as "
|
|
"driver does not support read-only mode rules.",
|
|
share_instance['id'])
|
|
driver_rules = add_rules
|
|
|
|
try:
|
|
access_keys = None
|
|
try:
|
|
access_keys = self.driver.update_access(
|
|
context,
|
|
share_instance,
|
|
driver_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'])
|