a1ba28cad3
We cannot deny ipv6 access rules because we remove the ipv6 access rules from global delete rules, not just remove the ipv6 access rules from driver update_access interface parameters. So the ipv6 access rules cannot be deleted in db. Now, changed to only remove the ipv6 access rules from the driver update_access interface parameters(add_rules, delete_rules, access_rules_to_be_on_share). Closes-bug: 1707066 Depends-On: Ifea1799e1d2e3963fec7e90ce3f9cb47b9f02f4f Change-Id: Idd0014d898d5468922625e62f9e649926dc04e35
579 lines
24 KiB
Python
579 lines
24 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.
|
|
|
|
import copy
|
|
import ipaddress
|
|
|
|
from oslo_log import log
|
|
|
|
from manila.common import constants
|
|
from manila.i18n import _
|
|
from manila import utils
|
|
|
|
import six
|
|
|
|
LOG = log.getLogger(__name__)
|
|
|
|
|
|
def locked_access_rules_operation(operation):
|
|
"""Lock decorator for access rules operations.
|
|
|
|
Takes a named lock prior to executing the operation. The lock is
|
|
named with the ID of the share instance to which the access rule belongs.
|
|
|
|
Intended use:
|
|
If an database operation to retrieve or update access rules uses this
|
|
decorator, it will block actions on all access rules of the share
|
|
instance until the named lock is free. This is used to avoid race
|
|
conditions while performing access rules updates on a given share instance.
|
|
"""
|
|
|
|
def wrapped(*args, **kwargs):
|
|
instance_id = kwargs.get('share_instance_id')
|
|
|
|
@utils.synchronized(
|
|
"locked_access_rules_operation_by_share_instance_%s" % instance_id,
|
|
external=True)
|
|
def locked_operation(*_args, **_kwargs):
|
|
return operation(*_args, **_kwargs)
|
|
|
|
return locked_operation(*args, **kwargs)
|
|
|
|
return wrapped
|
|
|
|
|
|
class ShareInstanceAccessDatabaseMixin(object):
|
|
|
|
@locked_access_rules_operation
|
|
def get_and_update_share_instance_access_rules_status(
|
|
self, context, status=None, conditionally_change=None,
|
|
share_instance_id=None):
|
|
"""Get and update the access_rules_status of a share instance.
|
|
|
|
:param status: Set this parameter only if you want to
|
|
omit the conditionally_change parameter; i.e, if you want to
|
|
force a state change on the share instance regardless of the prior
|
|
state.
|
|
:param conditionally_change: Set this parameter to a dictionary of rule
|
|
state transitions to be made. The key is the expected
|
|
access_rules_status and the value is the state to transition the
|
|
access_rules_status to. If the state is not as expected,
|
|
no transition is performed. Default is {}, which means no state
|
|
transitions will be made.
|
|
:returns share_instance: if an update was made.
|
|
"""
|
|
if status is not None:
|
|
updates = {'access_rules_status': status}
|
|
elif conditionally_change:
|
|
share_instance = self.db.share_instance_get(
|
|
context, share_instance_id)
|
|
access_rules_status = share_instance['access_rules_status']
|
|
try:
|
|
updates = {
|
|
'access_rules_status':
|
|
conditionally_change[access_rules_status],
|
|
}
|
|
except KeyError:
|
|
updates = {}
|
|
else:
|
|
updates = {}
|
|
if updates:
|
|
share_instance = self.db.share_instance_update(
|
|
context, share_instance_id, updates, with_share_data=True)
|
|
return share_instance
|
|
|
|
@locked_access_rules_operation
|
|
def get_and_update_share_instance_access_rules(self, context,
|
|
filters=None, updates=None,
|
|
conditionally_change=None,
|
|
share_instance_id=None):
|
|
"""Get and conditionally update all access rules of a share instance.
|
|
|
|
:param updates: Set this parameter to a dictionary of key:value
|
|
pairs corresponding to the keys in the ShareInstanceAccessMapping
|
|
model. Include 'state' in this dictionary only if you want to
|
|
omit the conditionally_change parameter; i.e, if you want to
|
|
force a state change on all filtered rules regardless of the prior
|
|
state. This parameter is always honored, regardless of whether
|
|
conditionally_change allows for a state transition as desired.
|
|
|
|
Example::
|
|
|
|
{
|
|
'access_key': 'bob007680048318f4239dfc1c192d5',
|
|
'access_level': 'ro',
|
|
}
|
|
|
|
:param conditionally_change: Set this parameter to a dictionary of rule
|
|
state transitions to be made. The key is the expected state of
|
|
the access rule the value is the state to transition the
|
|
access rule to. If the state is not as expected, no transition is
|
|
performed. Default is {}, which means no state transitions
|
|
will be made.
|
|
|
|
Example::
|
|
|
|
{
|
|
'queued_to_apply': 'applying',
|
|
'queued_to_deny': 'denying',
|
|
}
|
|
|
|
"""
|
|
instance_rules = self.db.share_access_get_all_for_instance(
|
|
context, share_instance_id, filters=filters)
|
|
|
|
if instance_rules and (updates or conditionally_change):
|
|
if not updates:
|
|
updates = {}
|
|
if not conditionally_change:
|
|
conditionally_change = {}
|
|
for rule in instance_rules:
|
|
mapping_state = rule['state']
|
|
rule_updates = copy.deepcopy(updates)
|
|
try:
|
|
rule_updates['state'] = conditionally_change[mapping_state]
|
|
except KeyError:
|
|
pass
|
|
if rule_updates:
|
|
self.db.share_instance_access_update(
|
|
context, rule['access_id'], share_instance_id,
|
|
rule_updates)
|
|
|
|
# Refresh the rules after the updates
|
|
rules_to_get = {
|
|
'access_id': tuple([i['access_id'] for i in instance_rules]),
|
|
}
|
|
instance_rules = self.db.share_access_get_all_for_instance(
|
|
context, share_instance_id, filters=rules_to_get)
|
|
|
|
return instance_rules
|
|
|
|
def get_share_instance_access_rules(self, context, filters=None,
|
|
share_instance_id=None):
|
|
return self.get_and_update_share_instance_access_rules(
|
|
context, filters, None, None, share_instance_id)
|
|
|
|
@locked_access_rules_operation
|
|
def get_and_update_share_instance_access_rule(self, context, rule_id,
|
|
updates=None,
|
|
share_instance_id=None,
|
|
conditionally_change=None):
|
|
"""Get and conditionally update a given share instance access rule.
|
|
|
|
:param updates: Set this parameter to a dictionary of key:value
|
|
pairs corresponding to the keys in the ShareInstanceAccessMapping
|
|
model. Include 'state' in this dictionary only if you want to
|
|
omit the conditionally_change parameter; i.e, if you want to
|
|
force a state change regardless of the prior state.
|
|
:param conditionally_change: Set this parameter to a dictionary of rule
|
|
state transitions to be made. The key is the expected state of
|
|
the access rule the value is the state to transition the
|
|
access rule to. If the state is not as expected, no transition is
|
|
performed. Default is {}, which means no state transitions
|
|
will be made.
|
|
|
|
Example::
|
|
|
|
{
|
|
'queued_to_apply': 'applying',
|
|
'queued_to_deny': 'denying',
|
|
}
|
|
"""
|
|
instance_rule_mapping = self.db.share_instance_access_get(
|
|
context, rule_id, share_instance_id)
|
|
|
|
if not updates:
|
|
updates = {}
|
|
if conditionally_change:
|
|
mapping_state = instance_rule_mapping['state']
|
|
try:
|
|
updated_state = conditionally_change[mapping_state]
|
|
updates.update({'state': updated_state})
|
|
except KeyError:
|
|
msg = ("The state of the access rule %(rule_id)s (allowing "
|
|
"access to share instance %(si)s) was not updated "
|
|
"because its state was modified by another operation.")
|
|
msg_payload = {
|
|
'si': share_instance_id,
|
|
'rule_id': rule_id,
|
|
}
|
|
LOG.debug(msg, msg_payload)
|
|
if updates:
|
|
self.db.share_instance_access_update(
|
|
context, rule_id, share_instance_id, updates)
|
|
|
|
# Refresh the rule after update
|
|
instance_rule_mapping = self.db.share_instance_access_get(
|
|
context, rule_id, share_instance_id)
|
|
|
|
return instance_rule_mapping
|
|
|
|
@locked_access_rules_operation
|
|
def delete_share_instance_access_rules(self, context, access_rules,
|
|
share_instance_id=None):
|
|
for rule in access_rules:
|
|
self.db.share_instance_access_delete(context, rule['id'])
|
|
|
|
|
|
class ShareInstanceAccess(ShareInstanceAccessDatabaseMixin):
|
|
|
|
def __init__(self, db, driver):
|
|
self.db = db
|
|
self.driver = driver
|
|
|
|
def update_access_rules(self, context, share_instance_id,
|
|
delete_all_rules=False, share_server=None):
|
|
"""Update access rules for a given share instance.
|
|
|
|
:param context: request context
|
|
:param share_instance_id: ID of the share instance
|
|
:param delete_all_rules: set this parameter to True if all
|
|
existing access rules must be denied for a given share instance
|
|
:param share_server: Share server model or None
|
|
"""
|
|
share_instance = self.db.share_instance_get(
|
|
context, share_instance_id, with_share_data=True)
|
|
msg_payload = {
|
|
'si': share_instance_id,
|
|
'shr': share_instance['share_id'],
|
|
}
|
|
|
|
if delete_all_rules:
|
|
updates = {
|
|
'state': constants.ACCESS_STATE_QUEUED_TO_DENY,
|
|
}
|
|
self.get_and_update_share_instance_access_rules(
|
|
context, updates=updates, share_instance_id=share_instance_id)
|
|
|
|
# Is there a sync in progress? If yes, ignore the incoming request.
|
|
rule_filter = {
|
|
'state': (constants.ACCESS_STATE_APPLYING,
|
|
constants.ACCESS_STATE_DENYING),
|
|
}
|
|
syncing_rules = self.get_and_update_share_instance_access_rules(
|
|
context, filters=rule_filter, share_instance_id=share_instance_id)
|
|
|
|
if syncing_rules:
|
|
msg = ("Access rules are being synced for share instance "
|
|
"%(si)s belonging to share %(shr)s, any rule changes will "
|
|
"be applied shortly.")
|
|
LOG.debug(msg, msg_payload)
|
|
else:
|
|
rules_to_apply_or_deny = (
|
|
self._update_and_get_unsynced_access_rules_from_db(
|
|
context, share_instance_id)
|
|
)
|
|
if rules_to_apply_or_deny:
|
|
msg = ("Updating access rules for share instance %(si)s "
|
|
"belonging to share %(shr)s.")
|
|
LOG.debug(msg, msg_payload)
|
|
self._update_access_rules(context, share_instance_id,
|
|
share_server=share_server)
|
|
else:
|
|
msg = ("All access rules have been synced for share instance "
|
|
"%(si)s belonging to share %(shr)s.")
|
|
LOG.debug(msg, msg_payload)
|
|
|
|
def _update_access_rules(self, context, share_instance_id,
|
|
share_server=None):
|
|
# Refresh the share instance model
|
|
share_instance = self.db.share_instance_get(
|
|
context, share_instance_id, with_share_data=True)
|
|
|
|
conditionally_change = {
|
|
constants.STATUS_ACTIVE: constants.SHARE_INSTANCE_RULES_SYNCING,
|
|
}
|
|
share_instance = (
|
|
self.get_and_update_share_instance_access_rules_status(
|
|
context, conditionally_change=conditionally_change,
|
|
share_instance_id=share_instance_id) or share_instance
|
|
)
|
|
|
|
rules_to_be_removed_from_db = []
|
|
# Populate rules to send to the driver
|
|
(access_rules_to_be_on_share, add_rules, delete_rules) = (
|
|
self._get_rules_to_send_to_driver(context, share_instance)
|
|
)
|
|
|
|
if share_instance['cast_rules_to_readonly']:
|
|
# Ensure read/only semantics for a migrating instances
|
|
access_rules_to_be_on_share = self._set_rules_to_readonly(
|
|
access_rules_to_be_on_share, share_instance)
|
|
add_rules = []
|
|
rules_to_be_removed_from_db = delete_rules
|
|
delete_rules = []
|
|
|
|
try:
|
|
driver_rule_updates = self._update_rules_through_share_driver(
|
|
context, share_instance, access_rules_to_be_on_share,
|
|
add_rules, delete_rules, rules_to_be_removed_from_db,
|
|
share_server)
|
|
|
|
self._process_driver_rule_updates(
|
|
context, driver_rule_updates, share_instance_id)
|
|
|
|
# Update access rules that are still in 'applying' state
|
|
conditionally_change = {
|
|
constants.ACCESS_STATE_APPLYING: constants.ACCESS_STATE_ACTIVE,
|
|
}
|
|
self.get_and_update_share_instance_access_rules(
|
|
context, share_instance_id=share_instance_id,
|
|
conditionally_change=conditionally_change)
|
|
|
|
except Exception:
|
|
conditionally_change_rule_state = {
|
|
constants.ACCESS_STATE_APPLYING: constants.ACCESS_STATE_ERROR,
|
|
constants.ACCESS_STATE_DENYING: constants.ACCESS_STATE_ERROR,
|
|
}
|
|
self.get_and_update_share_instance_access_rules(
|
|
context, share_instance_id=share_instance_id,
|
|
conditionally_change=conditionally_change_rule_state)
|
|
|
|
conditionally_change_access_rules_status = {
|
|
constants.ACCESS_STATE_ACTIVE: constants.STATUS_ERROR,
|
|
constants.SHARE_INSTANCE_RULES_SYNCING: constants.STATUS_ERROR,
|
|
}
|
|
self.get_and_update_share_instance_access_rules_status(
|
|
context, share_instance_id=share_instance_id,
|
|
conditionally_change=conditionally_change_access_rules_status)
|
|
raise
|
|
|
|
if rules_to_be_removed_from_db:
|
|
delete_rules = rules_to_be_removed_from_db
|
|
|
|
self.delete_share_instance_access_rules(
|
|
context, delete_rules, share_instance_id=share_instance['id'])
|
|
|
|
self._loop_for_refresh_else_update_access_rules_status(
|
|
context, share_instance_id, share_server)
|
|
|
|
msg = _("Access rules were successfully modified for share instance "
|
|
"%(si)s belonging to share %(shr)s.")
|
|
msg_payload = {
|
|
'si': share_instance['id'],
|
|
'shr': share_instance['share_id'],
|
|
}
|
|
LOG.info(msg, msg_payload)
|
|
|
|
def _update_rules_through_share_driver(self, context, share_instance,
|
|
access_rules_to_be_on_share,
|
|
add_rules, delete_rules,
|
|
rules_to_be_removed_from_db,
|
|
share_server):
|
|
driver_rule_updates = {}
|
|
share_protocol = share_instance['share_proto'].lower()
|
|
if (not self.driver.ipv6_implemented and
|
|
share_protocol == 'nfs'):
|
|
add_rules = self._filter_ipv6_rules(add_rules)
|
|
delete_rules = self._filter_ipv6_rules(delete_rules)
|
|
access_rules_to_be_on_share = self._filter_ipv6_rules(
|
|
access_rules_to_be_on_share)
|
|
try:
|
|
driver_rule_updates = self.driver.update_access(
|
|
context,
|
|
share_instance,
|
|
access_rules_to_be_on_share,
|
|
add_rules=add_rules,
|
|
delete_rules=delete_rules,
|
|
share_server=share_server
|
|
) or {}
|
|
except NotImplementedError:
|
|
# NOTE(u_glide): Fallback to legacy allow_access/deny_access
|
|
# for drivers without update_access() method support
|
|
self._update_access_fallback(context, add_rules, delete_rules,
|
|
rules_to_be_removed_from_db,
|
|
share_instance,
|
|
share_server)
|
|
return driver_rule_updates
|
|
|
|
def _loop_for_refresh_else_update_access_rules_status(self, context,
|
|
share_instance_id,
|
|
share_server):
|
|
# Do we need to re-sync or apply any new changes?
|
|
if self._check_needs_refresh(context, share_instance_id):
|
|
self._update_access_rules(context, share_instance_id,
|
|
share_server=share_server)
|
|
else:
|
|
# Switch the share instance's access_rules_status to 'active'
|
|
# if there are no more rules in 'error' state, else, ensure
|
|
# 'error' state.
|
|
rule_filter = {'state': constants.STATUS_ERROR}
|
|
rules_in_error_state = (
|
|
self.get_and_update_share_instance_access_rules(
|
|
context, filters=rule_filter,
|
|
share_instance_id=share_instance_id)
|
|
)
|
|
if not rules_in_error_state:
|
|
conditionally_change = {
|
|
constants.SHARE_INSTANCE_RULES_SYNCING:
|
|
constants.STATUS_ACTIVE,
|
|
constants.SHARE_INSTANCE_RULES_ERROR:
|
|
constants.STATUS_ACTIVE,
|
|
}
|
|
self.get_and_update_share_instance_access_rules_status(
|
|
context, conditionally_change=conditionally_change,
|
|
share_instance_id=share_instance_id)
|
|
else:
|
|
conditionally_change = {
|
|
constants.SHARE_INSTANCE_RULES_SYNCING:
|
|
constants.SHARE_INSTANCE_RULES_ERROR,
|
|
}
|
|
self.get_and_update_share_instance_access_rules_status(
|
|
context, conditionally_change=conditionally_change,
|
|
share_instance_id=share_instance_id)
|
|
|
|
def _process_driver_rule_updates(self, context, driver_rule_updates,
|
|
share_instance_id):
|
|
for rule_id, rule_updates in driver_rule_updates.items():
|
|
if 'state' in rule_updates:
|
|
# We allow updates *only* if the state is unchanged from
|
|
# the time this update was initiated. It is possible
|
|
# that the access rule was denied at the API prior to
|
|
# the driver reporting that the access rule was added
|
|
# successfully.
|
|
state = rule_updates.pop('state')
|
|
conditional_state_updates = {
|
|
constants.ACCESS_STATE_APPLYING: state,
|
|
constants.ACCESS_STATE_DENYING: state,
|
|
constants.ACCESS_STATE_ACTIVE: state,
|
|
}
|
|
else:
|
|
conditional_state_updates = {}
|
|
self.get_and_update_share_instance_access_rule(
|
|
context, rule_id, updates=rule_updates,
|
|
share_instance_id=share_instance_id,
|
|
conditionally_change=conditional_state_updates)
|
|
|
|
@staticmethod
|
|
def _set_rules_to_readonly(access_rules_to_be_on_share, share_instance):
|
|
|
|
LOG.debug("All access rules of share instance %s are being "
|
|
"cast to read-only for a migration or because the "
|
|
"instance is a readable replica.",
|
|
share_instance['id'])
|
|
|
|
for rule in access_rules_to_be_on_share:
|
|
rule['access_level'] = constants.ACCESS_LEVEL_RO
|
|
|
|
return access_rules_to_be_on_share
|
|
|
|
@staticmethod
|
|
def _filter_ipv6_rules(rules):
|
|
filtered = []
|
|
for rule in rules:
|
|
if rule['access_type'] == 'ip':
|
|
ip_version = ipaddress.ip_network(
|
|
six.text_type(rule['access_to'])).version
|
|
if 6 == ip_version:
|
|
continue
|
|
filtered.append(rule)
|
|
return filtered
|
|
|
|
def _get_rules_to_send_to_driver(self, context, share_instance):
|
|
add_rules = []
|
|
delete_rules = []
|
|
access_filters = {
|
|
'state': (constants.ACCESS_STATE_APPLYING,
|
|
constants.ACCESS_STATE_ACTIVE,
|
|
constants.ACCESS_STATE_DENYING),
|
|
}
|
|
existing_rules_in_db = self.get_and_update_share_instance_access_rules(
|
|
context, filters=access_filters,
|
|
share_instance_id=share_instance['id'])
|
|
# Update queued rules to transitional states
|
|
for rule in existing_rules_in_db:
|
|
|
|
if rule['state'] == constants.ACCESS_STATE_APPLYING:
|
|
add_rules.append(rule)
|
|
elif rule['state'] == constants.ACCESS_STATE_DENYING:
|
|
delete_rules.append(rule)
|
|
delete_rule_ids = [r['id'] for r in delete_rules]
|
|
access_rules_to_be_on_share = [
|
|
r for r in existing_rules_in_db if r['id'] not in delete_rule_ids
|
|
]
|
|
return access_rules_to_be_on_share, add_rules, delete_rules
|
|
|
|
def _check_needs_refresh(self, context, share_instance_id):
|
|
rules_to_apply_or_deny = (
|
|
self._update_and_get_unsynced_access_rules_from_db(
|
|
context, share_instance_id)
|
|
)
|
|
return any(rules_to_apply_or_deny)
|
|
|
|
def _update_access_fallback(self, context, add_rules, delete_rules,
|
|
remove_rules, share_instance, share_server):
|
|
for rule in add_rules:
|
|
LOG.info(
|
|
"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.extend(remove_rules)
|
|
|
|
for rule in delete_rules:
|
|
LOG.info(
|
|
"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 _update_and_get_unsynced_access_rules_from_db(self, context,
|
|
share_instance_id):
|
|
rule_filter = {
|
|
'state': (constants.ACCESS_STATE_QUEUED_TO_APPLY,
|
|
constants.ACCESS_STATE_QUEUED_TO_DENY),
|
|
}
|
|
conditionally_change = {
|
|
constants.ACCESS_STATE_QUEUED_TO_APPLY:
|
|
constants.ACCESS_STATE_APPLYING,
|
|
constants.ACCESS_STATE_QUEUED_TO_DENY:
|
|
constants.ACCESS_STATE_DENYING,
|
|
}
|
|
rules_to_apply_or_deny = (
|
|
self.get_and_update_share_instance_access_rules(
|
|
context, filters=rule_filter,
|
|
share_instance_id=share_instance_id,
|
|
conditionally_change=conditionally_change)
|
|
)
|
|
return rules_to_apply_or_deny
|
|
|
|
def reset_applying_rules(self, context, share_instance_id):
|
|
conditional_updates = {
|
|
constants.ACCESS_STATE_APPLYING:
|
|
constants.ACCESS_STATE_QUEUED_TO_APPLY,
|
|
constants.ACCESS_STATE_DENYING:
|
|
constants.ACCESS_STATE_QUEUED_TO_DENY,
|
|
}
|
|
self.get_and_update_share_instance_access_rules(
|
|
context, share_instance_id=share_instance_id,
|
|
conditionally_change=conditional_updates)
|