Allow node_sanitize function to be provided overrides

The biggest amount of API overhead is the node sanitization
process, at least at this point in time.

We have streamlined the database interaction to ensure specific
field selection lists are as orderly as possible, but the
node sanitization code re-executes some methods over and over
which do not require variable data from the underlying node.

These are blanket settings "is the user allowed to see x, or y".

Which means we can call node_sanitize pre-seeding these
arguments and execute the calls once, instead of a thousand times
to have the same exact result.

Story: 2008885
Task: 42433

Change-Id: I342e7900cac388cb4749480684418a5a15ac60eb
This commit is contained in:
Julia Kreger 2021-06-04 11:51:48 -07:00 committed by Jay Faulkner
parent 5a4783e4ac
commit cb1a5a3c98
3 changed files with 101 additions and 40 deletions

View File

@ -23,7 +23,8 @@ def has_next(collection, limit):
def list_convert_with_links(items, item_name, limit, url=None, fields=None,
sanitize_func=None, key_field='uuid', **kwargs):
sanitize_func=None, key_field='uuid',
sanitizer_args=None, **kwargs):
"""Build a collection dict including the next link for paging support.
:param items:
@ -41,6 +42,8 @@ def list_convert_with_links(items, item_name, limit, url=None, fields=None,
done in-place
:param key_field:
Key name for building next URL
:parm sanitizer_args:
Dictionary with additional arguments to be passed to the sanitizer.
:param kwargs:
other arguments passed to ``get_next``
:returns:
@ -55,8 +58,12 @@ def list_convert_with_links(items, item_name, limit, url=None, fields=None,
items_dict['next'] = next_uuid
if sanitize_func:
for item in items:
sanitize_func(item, fields=fields)
if sanitizer_args:
for item in items:
sanitize_func(item, fields, **sanitizer_args)
else:
for item in items:
sanitize_func(item, fields=fields)
return items_dict

View File

@ -1377,7 +1377,10 @@ def node_convert_with_links(rpc_node, fields=None, sanitize=True):
return node
def node_sanitize(node, fields):
def node_sanitize(node, fields, cdict=None,
show_driver_secrets=None,
show_instance_secrets=None,
evaluate_additional_policies=None):
"""Removes sensitive and unrequested data.
Will only keep the fields specified in the ``fields`` parameter.
@ -1385,13 +1388,37 @@ def node_sanitize(node, fields):
:param fields:
list of fields to preserve, or ``None`` to preserve them all
:type fields: list of str
:param cdict: Context dictionary for policy values evaluation.
If not provided, it will be executed by the method,
however for enumarting node lists, it is more efficent
to provide.
:param show_driver_secrets: A boolean value to allow external single
evaluation of policy instead of once per
node. Default None.
:param show_instance_secrets: A boolean value to allow external
evaluation of policy instead of once
per node. Default None.
:param evaluate_additional_policies: A boolean value to allow external
evaluation of policy instead of once
per node. Default None.
"""
# FIXME(TheJulia): As of ironic 18.0, this method is about 88% of
# NOTE(TheJulia): As of ironic 18.0, this method is about 88% of
# the time spent preparing to return a node to. If it takes us
# ~ 4.5 seconds to get 1000 nodes, we spend approximately 4 seconds
# PER 1000 in this call.
cdict = api.request.context.to_policy_values()
# PER 1000 in this call. When the calling method provides
# cdict, show_driver_secrets, show_instane_secrets, and
# evaluate_additional_policies, then performance of this method takes
# roughly half of the time, but performance increases in excess of 200%
# as policy checks are costly.
if not cdict:
cdict = api.request.context.to_policy_values()
# We need a new target_dict for each node as owner/lessee field have
# explicit associations and target comparison.
target_dict = dict(cdict)
# These fields are node specific.
owner = node.get('owner')
lessee = node.get('lessee')
@ -1410,12 +1437,12 @@ def node_sanitize(node, fields):
# NOTE(TheJulia): These methods use policy.check and normally return
# False in a noauth or password auth based situation, because the
# effective caller doesn't match the policy check rule.
show_driver_secrets = policy.check("show_password", cdict, target_dict)
show_instance_secrets = policy.check("show_instance_secrets",
cdict, target_dict)
# FIXME(TheJulia): The above two methods take approximately 20% of the
# present execution time. This needs to be more efficent as they do not
# need to be called for *every* node.
if show_driver_secrets is None:
show_driver_secrets = policy.check("show_password",
cdict, target_dict)
if show_instance_secrets is None:
show_instance_secrets = policy.check("show_instance_secrets",
cdict, target_dict)
# TODO(TheJulia): The above checks need to be migrated in some direction,
# but until we have auditing clarity, it might not be a big deal.
@ -1425,37 +1452,17 @@ def node_sanitize(node, fields):
# to keep the policy checks for say system-member based roles to
# a minimum as they are likely the regular API users as well.
# Also, the default for the filter_threshold is system-member.
evaluate_additional_policies = not policy.check_policy(
"baremetal:node:get:filter_threshold",
target_dict, cdict)
if evaluate_additional_policies is None:
evaluate_additional_policies = not policy.check_policy(
"baremetal:node:get:filter_threshold",
target_dict, cdict)
node_keys = node.keys()
if evaluate_additional_policies:
# NOTE(TheJulia): The net effect of this is that by default,
# at least matching common/policy.py defaults. is these should
# be stripped out.
if ('last_error' in node_keys
and not policy.check("baremetal:node:get:last_error",
target_dict, cdict)):
# Guard the last error from being visible as it can contain
# hostnames revealing infrastucture internal details.
node['last_error'] = ('** Value Redacted - Requires '
'baremetal:node:get:last_error '
'permission. **')
if ('reservation' in node_keys
and not policy.check("baremetal:node:get:reservation",
target_dict, cdict)):
# Guard conductor names from being visible.
node['reservation'] = ('** Redacted - requires baremetal:'
'node:get:reservation permission. **')
if ('driver_internal_info' in node_keys
and not policy.check("baremetal:node:get:driver_internal_info",
target_dict, cdict)):
# Guard conductor names from being visible.
node['driver_internal_info'] = {
'content': '** Redacted - Requires baremetal:node:get:'
'driver_internal_info permission. **'}
# Perform extended sanitization of nodes based upon policy
# baremetal:node:get:filter_threshold
_node_sanitize_extended(node, node_keys, target_dict, cdict)
if 'driver_info' in node_keys:
if (evaluate_additional_policies
@ -1502,8 +1509,48 @@ def node_sanitize(node, fields):
node.pop('states', None)
def _node_sanitize_extended(node, node_keys, target_dict, cdict):
# NOTE(TheJulia): The net effect of this is that by default,
# at least matching common/policy.py defaults. is these should
# be stripped out.
if ('last_error' in node_keys
and not policy.check("baremetal:node:get:last_error",
target_dict, cdict)):
# Guard the last error from being visible as it can contain
# hostnames revealing infrastucture internal details.
node['last_error'] = ('** Value Redacted - Requires '
'baremetal:node:get:last_error '
'permission. **')
if ('reservation' in node_keys
and not policy.check("baremetal:node:get:reservation",
target_dict, cdict)):
# Guard conductor names from being visible.
node['reservation'] = ('** Redacted - requires baremetal:'
'node:get:reservation permission. **')
if ('driver_internal_info' in node_keys
and not policy.check("baremetal:node:get:driver_internal_info",
target_dict, cdict)):
# Guard conductor names from being visible.
node['driver_internal_info'] = {
'content': '** Redacted - Requires baremetal:node:get:'
'driver_internal_info permission. **'}
def node_list_convert_with_links(nodes, limit, url=None, fields=None,
**kwargs):
cdict = api.request.context.to_policy_values()
target_dict = dict(cdict)
sanitizer_args = {
'cdict': cdict,
'show_driver_secrets': policy.check("show_password", cdict,
target_dict),
'show_instance_secrets': policy.check("show_instance_secrets",
cdict, target_dict),
'evaluate_additional_policies': not policy.check_policy(
"baremetal:node:get:filter_threshold",
target_dict, cdict),
}
return collection.list_convert_with_links(
items=[node_convert_with_links(n, fields=fields,
sanitize=False)
@ -1513,6 +1560,7 @@ def node_list_convert_with_links(nodes, limit, url=None, fields=None,
url=url,
fields=fields,
sanitize_func=node_sanitize,
sanitizer_args=sanitizer_args,
**kwargs
)

View File

@ -0,0 +1,6 @@
---
fixes:
- |
Improves record retrieval performance for baremetal nodes by enabling
ironic to not make redundant calls as part of generating API result
sets for the baremetal nodes endpoint.