Coverage for manila/share/access.py: 92%
219 statements
« prev ^ index » next coverage.py v7.11.0, created at 2026-02-18 22:19 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2026-02-18 22:19 +0000
1# Copyright (c) 2015 Mirantis Inc.
2# All Rights Reserved.
3#
4# Licensed under the Apache License, Version 2.0 (the "License"); you may
5# not use this file except in compliance with the License. You may obtain
6# a copy of the License at
7#
8# http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13# License for the specific language governing permissions and limitations
14# under the License.
16import copy
17import ipaddress
19from oslo_log import log
21from manila.common import constants
22from manila.i18n import _
23from manila import utils
25LOG = log.getLogger(__name__)
28def locked_access_rules_operation(operation):
29 """Lock decorator for access rules operations.
31 Takes a named lock prior to executing the operation. The lock is
32 named with the ID of the share instance to which the access rule belongs.
34 Intended use:
35 If an database operation to retrieve or update access rules uses this
36 decorator, it will block actions on all access rules of the share
37 instance until the named lock is free. This is used to avoid race
38 conditions while performing access rules updates on a given share instance.
39 """
41 def wrapped(*args, **kwargs):
42 instance_id = kwargs.get('share_instance_id')
44 @utils.synchronized(
45 "locked_access_rules_operation_by_share_instance_%s" % instance_id,
46 external=True)
47 def locked_operation(*_args, **_kwargs):
48 return operation(*_args, **_kwargs)
50 return locked_operation(*args, **kwargs)
52 return wrapped
55class ShareInstanceAccessDatabaseMixin(object):
57 @locked_access_rules_operation
58 def get_and_update_share_instance_access_rules_status(
59 self, context, status=None, conditionally_change=None,
60 share_instance_id=None):
61 """Get and update the access_rules_status of a share instance.
63 :param status: Set this parameter only if you want to
64 omit the conditionally_change parameter; i.e, if you want to
65 force a state change on the share instance regardless of the prior
66 state.
67 :param conditionally_change: Set this parameter to a dictionary of rule
68 state transitions to be made. The key is the expected
69 access_rules_status and the value is the state to transition the
70 access_rules_status to. If the state is not as expected,
71 no transition is performed. Default is {}, which means no state
72 transitions will be made.
73 :returns share_instance: if an update was made.
74 """
75 if status is not None:
76 updates = {'access_rules_status': status}
77 elif conditionally_change: 77 ↛ 89line 77 didn't jump to line 89 because the condition on line 77 was always true
78 share_instance = self.db.share_instance_get(
79 context, share_instance_id)
80 access_rules_status = share_instance['access_rules_status']
81 try:
82 updates = {
83 'access_rules_status':
84 conditionally_change[access_rules_status],
85 }
86 except KeyError:
87 updates = {}
88 else:
89 updates = {}
90 if updates:
91 share_instance = self.db.share_instance_update(
92 context, share_instance_id, updates, with_share_data=True)
93 return share_instance
95 def update_share_instances_access_rules_status(
96 self, context, status, share_instance_ids):
97 """Update the access_rules_status of all share instances.
99 .. note::
100 Before making this call, make sure that all share instances have
101 their status set to a value that will block new operations to
102 happen during this update.
104 :param status: Force a state change on all share instances regardless
105 of the prior state.
106 :param share_instance_ids: List of share instance ids to have their
107 access rules status updated.
108 """
109 updates = {'access_rules_status': status}
111 self.db.share_instance_status_update(
112 context, share_instance_ids, updates)
114 @locked_access_rules_operation
115 def get_and_update_share_instance_access_rules(self, context,
116 filters=None, updates=None,
117 conditionally_change=None,
118 share_instance_id=None):
119 """Get and conditionally update all access rules of a share instance.
121 :param updates: Set this parameter to a dictionary of key:value
122 pairs corresponding to the keys in the ShareInstanceAccessMapping
123 model. Include 'state' in this dictionary only if you want to
124 omit the conditionally_change parameter; i.e, if you want to
125 force a state change on all filtered rules regardless of the prior
126 state. This parameter is always honored, regardless of whether
127 conditionally_change allows for a state transition as desired.
129 Example::
131 {
132 'access_key': 'bob007680048318f4239dfc1c192d5',
133 'access_level': 'ro',
134 }
136 :param conditionally_change: Set this parameter to a dictionary of rule
137 state transitions to be made. The key is the expected state of
138 the access rule the value is the state to transition the
139 access rule to. If the state is not as expected, no transition is
140 performed. Default is {}, which means no state transitions
141 will be made.
143 Example::
145 {
146 'queued_to_apply': 'applying',
147 'queued_to_deny': 'denying',
148 }
150 """
151 instance_rules = self.db.share_access_get_all_for_instance(
152 context, share_instance_id, filters=filters)
154 if instance_rules and (updates or conditionally_change):
155 if not updates:
156 updates = {}
157 if not conditionally_change:
158 conditionally_change = {}
159 for rule in instance_rules:
160 mapping_state = rule['state']
161 rule_updates = copy.deepcopy(updates)
162 try:
163 rule_updates['state'] = conditionally_change[mapping_state]
164 except KeyError:
165 pass
166 if rule_updates:
167 self.db.share_instance_access_update(
168 context, rule['access_id'], share_instance_id,
169 rule_updates)
171 # Refresh the rules after the updates
172 rules_to_get = {
173 'access_id': tuple([i['access_id'] for i in instance_rules]),
174 }
175 instance_rules = self.db.share_access_get_all_for_instance(
176 context, share_instance_id, filters=rules_to_get)
178 return instance_rules
180 def get_share_instance_access_rules(self, context, filters=None,
181 share_instance_id=None):
182 return self.get_and_update_share_instance_access_rules(
183 context, filters, None, None, share_instance_id)
185 @locked_access_rules_operation
186 def get_and_update_share_instance_access_rule(self, context, rule_id,
187 updates=None,
188 share_instance_id=None,
189 conditionally_change=None):
190 """Get and conditionally update a given share instance access rule.
192 :param updates: Set this parameter to a dictionary of key:value
193 pairs corresponding to the keys in the ShareInstanceAccessMapping
194 model. Include 'state' in this dictionary only if you want to
195 omit the conditionally_change parameter; i.e, if you want to
196 force a state change regardless of the prior state.
197 :param conditionally_change: Set this parameter to a dictionary of rule
198 state transitions to be made. The key is the expected state of
199 the access rule the value is the state to transition the
200 access rule to. If the state is not as expected, no transition is
201 performed. Default is {}, which means no state transitions
202 will be made.
204 Example::
206 {
207 'queued_to_apply': 'applying',
208 'queued_to_deny': 'denying',
209 }
210 """
211 instance_rule_mapping = self.db.share_instance_access_get(
212 context, rule_id, share_instance_id)
214 if not updates:
215 updates = {}
216 if conditionally_change:
217 mapping_state = instance_rule_mapping['state']
218 try:
219 updated_state = conditionally_change[mapping_state]
220 updates.update({'state': updated_state})
221 except KeyError:
222 msg = ("The state of the access rule %(rule_id)s (allowing "
223 "access to share instance %(si)s) was not updated "
224 "because its state was modified by another operation.")
225 msg_payload = {
226 'si': share_instance_id,
227 'rule_id': rule_id,
228 }
229 LOG.debug(msg, msg_payload)
230 if updates:
231 self.db.share_instance_access_update(
232 context, rule_id, share_instance_id, updates)
234 # Refresh the rule after update
235 instance_rule_mapping = self.db.share_instance_access_get(
236 context, rule_id, share_instance_id)
238 return instance_rule_mapping
240 @locked_access_rules_operation
241 def delete_share_instance_access_rules(self, context, access_rules,
242 share_instance_id=None):
243 for rule in access_rules:
244 self.db.share_instance_access_delete(context, rule['id'])
247class ShareInstanceAccess(ShareInstanceAccessDatabaseMixin):
249 def __init__(self, db, driver):
250 self.db = db
251 self.driver = driver
253 def update_access_rules(self, context, share_instance_id,
254 delete_all_rules=False, share_server=None):
255 """Update access rules for a given share instance.
257 :param context: request context
258 :param share_instance_id: ID of the share instance
259 :param delete_all_rules: set this parameter to True if all
260 existing access rules must be denied for a given share instance
261 :param share_server: Share server model or None
262 """
263 share_instance = self.db.share_instance_get(
264 context, share_instance_id, with_share_data=True)
265 msg_payload = {
266 'si': share_instance_id,
267 'shr': share_instance['share_id'],
268 }
270 if delete_all_rules:
271 updates = {
272 'state': constants.ACCESS_STATE_QUEUED_TO_DENY,
273 }
274 self.get_and_update_share_instance_access_rules(
275 context, updates=updates, share_instance_id=share_instance_id)
277 # Is there a sync in progress? If yes, ignore the incoming request.
278 rule_filter = {
279 'state': (constants.ACCESS_STATE_APPLYING,
280 constants.ACCESS_STATE_DENYING,
281 constants.ACCESS_STATE_UPDATING),
282 }
283 syncing_rules = self.get_and_update_share_instance_access_rules(
284 context, filters=rule_filter, share_instance_id=share_instance_id)
286 if syncing_rules:
287 msg = ("Access rules are being synced for share instance "
288 "%(si)s belonging to share %(shr)s, any rule changes will "
289 "be applied shortly.")
290 LOG.debug(msg, msg_payload)
291 else:
292 rules_to_apply_or_update_or_deny = (
293 self._update_and_get_unsynced_access_rules_from_db(
294 context, share_instance_id)
295 )
296 if rules_to_apply_or_update_or_deny:
297 msg = ("Updating access rules for share instance %(si)s "
298 "belonging to share %(shr)s.")
299 LOG.debug(msg, msg_payload)
300 self._update_access_rules(context, share_instance_id,
301 share_server=share_server)
302 else:
303 msg = ("All access rules have been synced for share instance "
304 "%(si)s belonging to share %(shr)s.")
305 LOG.debug(msg, msg_payload)
307 def _update_access_rules(self, context, share_instance_id,
308 share_server=None):
309 # Refresh the share instance model
310 share_instance = self.db.share_instance_get(
311 context, share_instance_id, with_share_data=True)
313 conditionally_change = {
314 constants.STATUS_ACTIVE: constants.SHARE_INSTANCE_RULES_SYNCING,
315 }
316 share_instance = (
317 self.get_and_update_share_instance_access_rules_status(
318 context, conditionally_change=conditionally_change,
319 share_instance_id=share_instance_id) or share_instance
320 )
322 rules_to_be_removed_from_db = []
323 # Populate rules to send to the driver
324 (access_rules_on_share, add_rules, delete_rules, update_rules) = (
325 self._get_rules_to_send_to_driver(context, share_instance)
326 )
328 if share_instance['cast_rules_to_readonly']:
329 # Ensure read/only semantics for a migrating instances
330 access_rules_on_share = self._set_rules_to_readonly(
331 access_rules_on_share, share_instance)
332 add_rules = []
333 rules_to_be_removed_from_db = delete_rules
334 delete_rules = []
335 update_rules = []
337 try:
338 share_instance = share_instance.to_dict()
339 metadata = self.db.share_metadata_get(
340 context, share_instance['share_id'])
341 if metadata: 341 ↛ 342line 341 didn't jump to line 342 because the condition on line 341 was never true
342 share_instance.update({'metadata': metadata})
344 driver_rule_updates = self._update_rules_through_share_driver(
345 context, share_instance, access_rules_on_share,
346 add_rules, delete_rules, update_rules,
347 rules_to_be_removed_from_db,
348 share_server)
350 self.process_driver_rule_updates(
351 context, driver_rule_updates, share_instance_id)
353 # Update access rules that are still in 'applying/updating' state
354 conditionally_change = {
355 constants.ACCESS_STATE_APPLYING: constants.ACCESS_STATE_ACTIVE,
356 constants.ACCESS_STATE_UPDATING: constants.ACCESS_STATE_ACTIVE,
357 }
358 self.get_and_update_share_instance_access_rules(
359 context, share_instance_id=share_instance_id,
360 conditionally_change=conditionally_change)
362 except Exception:
363 conditionally_change_rule_state = {
364 constants.ACCESS_STATE_APPLYING: constants.ACCESS_STATE_ERROR,
365 constants.ACCESS_STATE_DENYING: constants.ACCESS_STATE_ERROR,
366 constants.ACCESS_STATE_UPDATING: constants.ACCESS_STATE_ERROR,
367 }
368 self.get_and_update_share_instance_access_rules(
369 context, share_instance_id=share_instance_id,
370 conditionally_change=conditionally_change_rule_state)
372 conditionally_change_access_rules_status = {
373 constants.ACCESS_STATE_ACTIVE: constants.STATUS_ERROR,
374 constants.SHARE_INSTANCE_RULES_SYNCING: constants.STATUS_ERROR,
375 }
376 self.get_and_update_share_instance_access_rules_status(
377 context, share_instance_id=share_instance_id,
378 conditionally_change=conditionally_change_access_rules_status)
379 raise
381 if rules_to_be_removed_from_db: 381 ↛ 382line 381 didn't jump to line 382 because the condition on line 381 was never true
382 delete_rules = rules_to_be_removed_from_db
384 self.delete_share_instance_access_rules(
385 context, delete_rules, share_instance_id=share_instance['id'])
387 self._loop_for_refresh_else_update_access_rules_status(
388 context, share_instance_id, share_server)
390 msg = _("Access rules were successfully modified for share instance "
391 "%(si)s belonging to share %(shr)s.")
392 msg_payload = {
393 'si': share_instance['id'],
394 'shr': share_instance['share_id'],
395 }
396 LOG.info(msg, msg_payload)
398 def _update_rules_through_share_driver(self, context, share_instance,
399 access_rules_to_be_on_share,
400 add_rules, delete_rules,
401 update_rules,
402 rules_to_be_removed_from_db,
403 share_server):
404 driver_rule_updates = {}
405 share_protocol = share_instance['share_proto'].lower()
406 if (not self.driver.ipv6_implemented and
407 share_protocol == 'nfs'):
408 add_rules = self._filter_ipv6_rules(add_rules)
409 delete_rules = self._filter_ipv6_rules(delete_rules)
410 update_rules = self._filter_ipv6_rules(update_rules)
411 access_rules_to_be_on_share = self._filter_ipv6_rules(
412 access_rules_to_be_on_share)
413 try:
414 driver_rule_updates = self.driver.update_access(
415 context,
416 share_instance,
417 access_rules_to_be_on_share,
418 add_rules=add_rules,
419 delete_rules=delete_rules,
420 update_rules=update_rules,
421 share_server=share_server
422 ) or {}
423 except NotImplementedError:
424 # NOTE(u_glide): Fallback to legacy allow_access/deny_access
425 # for drivers without update_access() method support
426 # It is also possible that updating the access_level is not
427 # permitted.
428 self._update_access_fallback(context, add_rules, delete_rules,
429 rules_to_be_removed_from_db,
430 share_instance,
431 share_server)
432 return driver_rule_updates
434 def _loop_for_refresh_else_update_access_rules_status(self, context,
435 share_instance_id,
436 share_server):
437 # Do we need to re-sync or apply any new changes?
438 if self._check_needs_refresh(context, share_instance_id):
439 self._update_access_rules(context, share_instance_id,
440 share_server=share_server)
441 else:
442 # Switch the share instance's access_rules_status to 'active'
443 # if there are no more rules in 'error' state, else, ensure
444 # 'error' state.
445 rule_filter = {'state': constants.STATUS_ERROR}
446 rules_in_error_state = (
447 self.get_and_update_share_instance_access_rules(
448 context, filters=rule_filter,
449 share_instance_id=share_instance_id)
450 )
451 if not rules_in_error_state:
452 conditionally_change = {
453 constants.SHARE_INSTANCE_RULES_SYNCING:
454 constants.STATUS_ACTIVE,
455 constants.SHARE_INSTANCE_RULES_ERROR:
456 constants.STATUS_ACTIVE,
457 }
458 self.get_and_update_share_instance_access_rules_status(
459 context, conditionally_change=conditionally_change,
460 share_instance_id=share_instance_id)
461 else:
462 conditionally_change = {
463 constants.SHARE_INSTANCE_RULES_SYNCING:
464 constants.SHARE_INSTANCE_RULES_ERROR,
465 }
466 self.get_and_update_share_instance_access_rules_status(
467 context, conditionally_change=conditionally_change,
468 share_instance_id=share_instance_id)
470 def process_driver_rule_updates(self, context, driver_rule_updates,
471 share_instance_id):
472 for rule_id, rule_updates in driver_rule_updates.items():
473 if 'state' in rule_updates:
474 # We allow updates *only* if the state is unchanged from
475 # the time this update was initiated. It is possible
476 # that the access rule was denied at the API prior to
477 # the driver reporting that the access rule was added
478 # successfully.
479 state = rule_updates.pop('state')
480 conditional_state_updates = {
481 constants.ACCESS_STATE_APPLYING: state,
482 constants.ACCESS_STATE_DENYING: state,
483 constants.ACCESS_STATE_UPDATING: state,
484 constants.ACCESS_STATE_ACTIVE: state,
485 }
486 else:
487 conditional_state_updates = {}
488 self.get_and_update_share_instance_access_rule(
489 context, rule_id, updates=rule_updates,
490 share_instance_id=share_instance_id,
491 conditionally_change=conditional_state_updates)
493 @staticmethod
494 def _set_rules_to_readonly(access_rules_to_be_on_share, share_instance):
496 LOG.debug("All access rules of share instance %s are being "
497 "cast to read-only for a migration or because the "
498 "instance is a readable replica.",
499 share_instance['id'])
501 for rule in access_rules_to_be_on_share:
502 rule['access_level'] = constants.ACCESS_LEVEL_RO
504 return access_rules_to_be_on_share
506 @staticmethod
507 def _filter_ipv6_rules(rules):
508 filtered = []
509 for rule in rules:
510 if rule['access_type'] == 'ip':
511 ip_version = ipaddress.ip_network(
512 str(rule['access_to'])).version
513 if 6 == ip_version:
514 continue
515 filtered.append(rule)
516 return filtered
518 def _get_rules_to_send_to_driver(self, context, share_instance):
519 add_rules = []
520 delete_rules = []
521 update_rules = []
522 access_filters = {
523 'state': (constants.ACCESS_STATE_APPLYING,
524 constants.ACCESS_STATE_ACTIVE,
525 constants.ACCESS_STATE_DENYING,
526 constants.ACCESS_STATE_UPDATING),
527 }
528 existing_rules_in_db = self.get_and_update_share_instance_access_rules(
529 context, filters=access_filters,
530 share_instance_id=share_instance['id'])
531 # Update queued rules to transitional states
532 for rule in existing_rules_in_db:
534 if rule['state'] == constants.ACCESS_STATE_APPLYING:
535 add_rules.append(rule)
536 elif rule['state'] == constants.ACCESS_STATE_DENYING:
537 delete_rules.append(rule)
538 elif rule['state'] == constants.ACCESS_STATE_UPDATING: 538 ↛ 539line 538 didn't jump to line 539 because the condition on line 538 was never true
539 update_rules.append(rule)
540 delete_rule_ids = [r['id'] for r in delete_rules]
541 access_rules_to_be_on_share = [
542 r for r in existing_rules_in_db if r['id'] not in delete_rule_ids
543 ]
544 return (access_rules_to_be_on_share, add_rules,
545 delete_rules, update_rules)
547 def _check_needs_refresh(self, context, share_instance_id):
548 rules_to_apply_or_deny = (
549 self._update_and_get_unsynced_access_rules_from_db(
550 context, share_instance_id)
551 )
552 return any(rules_to_apply_or_deny)
554 def _update_access_fallback(self, context, add_rules, delete_rules,
555 remove_rules, share_instance, share_server):
556 for rule in add_rules:
557 LOG.info(
558 "Applying access rule '%(rule)s' for share "
559 "instance '%(instance)s'",
560 {'rule': rule['id'], 'instance': share_instance['id']}
561 )
563 self.driver.allow_access(
564 context,
565 share_instance,
566 rule,
567 share_server=share_server
568 )
570 # NOTE(ganso): Fallback mode temporary compatibility workaround
571 if remove_rules:
572 delete_rules.extend(remove_rules)
574 for rule in delete_rules:
575 LOG.info(
576 "Denying access rule '%(rule)s' from share "
577 "instance '%(instance)s'",
578 {'rule': rule['id'], 'instance': share_instance['id']}
579 )
581 self.driver.deny_access(
582 context,
583 share_instance,
584 rule,
585 share_server=share_server
586 )
588 def _update_and_get_unsynced_access_rules_from_db(self, context,
589 share_instance_id):
590 rule_filter = {
591 'state': (constants.ACCESS_STATE_QUEUED_TO_APPLY,
592 constants.ACCESS_STATE_QUEUED_TO_DENY,
593 constants.ACCESS_STATE_QUEUED_TO_UPDATE),
594 }
595 conditionally_change = {
596 constants.ACCESS_STATE_QUEUED_TO_APPLY:
597 constants.ACCESS_STATE_APPLYING,
598 constants.ACCESS_STATE_QUEUED_TO_DENY:
599 constants.ACCESS_STATE_DENYING,
600 constants.ACCESS_STATE_QUEUED_TO_UPDATE:
601 constants.ACCESS_STATE_UPDATING,
602 }
603 rules_to_apply_or_deny = (
604 self.get_and_update_share_instance_access_rules(
605 context, filters=rule_filter,
606 share_instance_id=share_instance_id,
607 conditionally_change=conditionally_change)
608 )
609 return rules_to_apply_or_deny
611 def reset_rules_to_queueing_states(self, context, share_instance_id,
612 reset_active=False):
613 """Reset applying and denying rules to queued states.
615 This helper is useful when re-applying rule changes.
616 :param context: the RequestContext object
617 :param share_instance_id: ID of the share instance
618 :param reset_active: If True, set "active" rules to "queued_to_apply"
619 """
620 conditional_updates = {
621 constants.ACCESS_STATE_APPLYING:
622 constants.ACCESS_STATE_QUEUED_TO_APPLY,
623 constants.ACCESS_STATE_DENYING:
624 constants.ACCESS_STATE_QUEUED_TO_DENY,
625 }
626 if reset_active:
627 conditional_updates.update({
628 constants.STATUS_ACTIVE:
629 constants.ACCESS_STATE_QUEUED_TO_APPLY,
630 })
631 self.get_and_update_share_instance_access_rules_status(
632 context,
633 share_instance_id=share_instance_id,
634 conditionally_change={
635 constants.SHARE_INSTANCE_RULES_ERROR:
636 constants.SHARE_INSTANCE_RULES_SYNCING,
637 constants.ACCESS_STATE_ACTIVE:
638 constants.SHARE_INSTANCE_RULES_SYNCING,
639 },
640 )
641 self.get_and_update_share_instance_access_rules(
642 context, share_instance_id=share_instance_id,
643 conditionally_change=conditional_updates)