summaryrefslogtreecommitdiff
path: root/lib/ansible/module_utils/common/parameters.py
diff options
context:
space:
mode:
Diffstat (limited to 'lib/ansible/module_utils/common/parameters.py')
-rw-r--r--lib/ansible/module_utils/common/parameters.py810
1 files changed, 423 insertions, 387 deletions
diff --git a/lib/ansible/module_utils/common/parameters.py b/lib/ansible/module_utils/common/parameters.py
index 4fa5dab84c..e297573410 100644
--- a/lib/ansible/module_utils/common/parameters.py
+++ b/lib/ansible/module_utils/common/parameters.py
@@ -15,6 +15,22 @@ from ansible.module_utils.common.collections import is_iterable
from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
from ansible.module_utils.common.text.formatters import lenient_lowercase
from ansible.module_utils.common.warnings import warn
+from ansible.module_utils.errors import (
+ AliasError,
+ AnsibleFallbackNotFound,
+ AnsibleValidationErrorMultiple,
+ ArgumentTypeError,
+ ArgumentValueError,
+ ElementError,
+ MutuallyExclusiveError,
+ NoLogError,
+ RequiredByError,
+ RequiredError,
+ RequiredIfError,
+ RequiredOneOfError,
+ RequiredTogetherError,
+ SubParameterTypeError,
+)
from ansible.module_utils.parsing.convert_bool import BOOLEANS_FALSE, BOOLEANS_TRUE
from ansible.module_utils.common._collections_compat import (
@@ -59,6 +75,13 @@ from ansible.module_utils.common.validation import (
# Python2 & 3 way to get NoneType
NoneType = type(None)
+_ADDITIONAL_CHECKS = (
+ {'func': check_required_together, 'attr': 'required_together', 'err': RequiredTogetherError},
+ {'func': check_required_one_of, 'attr': 'required_one_of', 'err': RequiredOneOfError},
+ {'func': check_required_if, 'attr': 'required_if', 'err': RequiredIfError},
+ {'func': check_required_by, 'attr': 'required_by', 'err': RequiredByError},
+)
+
# if adding boolean attribute, also add to PASS_BOOL
# some of this dupes defaults from controller config
PASS_VARS = {
@@ -97,8 +120,221 @@ DEFAULT_TYPE_VALIDATORS = {
}
-class AnsibleFallbackNotFound(Exception):
- pass
+def _get_type_validator(wanted):
+ """Returns the callable used to validate a wanted type and the type name.
+
+ :arg wanted: String or callable. If a string, get the corresponding
+ validation function from DEFAULT_TYPE_VALIDATORS. If callable,
+ get the name of the custom callable and return that for the type_checker.
+
+ :returns: Tuple of callable function or None, and a string that is the name
+ of the wanted type.
+ """
+
+ # Use one our our builtin validators.
+ if not callable(wanted):
+ if wanted is None:
+ # Default type for parameters
+ wanted = 'str'
+
+ type_checker = DEFAULT_TYPE_VALIDATORS.get(wanted)
+
+ # Use the custom callable for validation.
+ else:
+ type_checker = wanted
+ wanted = getattr(wanted, '__name__', to_native(type(wanted)))
+
+ return type_checker, wanted
+
+
+def _get_legal_inputs(argument_spec, parameters, aliases=None):
+ if aliases is None:
+ aliases = _handle_aliases(argument_spec, parameters)
+
+ return list(aliases.keys()) + list(argument_spec.keys())
+
+
+def _get_unsupported_parameters(argument_spec, parameters, legal_inputs=None, options_context=None):
+ """Check keys in parameters against those provided in legal_inputs
+ to ensure they contain legal values. If legal_inputs are not supplied,
+ they will be generated using the argument_spec.
+
+ :arg argument_spec: Dictionary of parameters, their type, and valid values.
+ :arg parameters: Dictionary of parameters.
+ :arg legal_inputs: List of valid key names property names. Overrides values
+ in argument_spec.
+ :arg options_context: List of parent keys for tracking the context of where
+ a parameter is defined.
+
+ :returns: Set of unsupported parameters. Empty set if no unsupported parameters
+ are found.
+ """
+
+ if legal_inputs is None:
+ legal_inputs = _get_legal_inputs(argument_spec, parameters)
+
+ unsupported_parameters = set()
+ for k in parameters.keys():
+ if k not in legal_inputs:
+ context = k
+ if options_context:
+ context = tuple(options_context + [k])
+
+ unsupported_parameters.add(context)
+
+ return unsupported_parameters
+
+
+def _handle_aliases(argument_spec, parameters, alias_warnings=None, alias_deprecations=None):
+ """Process aliases from an argument_spec including warnings and deprecations.
+
+ Modify ``parameters`` by adding a new key for each alias with the supplied
+ value from ``parameters``.
+
+ If a list is provided to the alias_warnings parameter, it will be filled with tuples
+ (option, alias) in every case where both an option and its alias are specified.
+
+ If a list is provided to alias_deprecations, it will be populated with dictionaries,
+ each containing deprecation information for each alias found in argument_spec.
+
+ :param argument_spec: Dictionary of parameters, their type, and valid values.
+ :type argument_spec: dict
+
+ :param parameters: Dictionary of parameters.
+ :type parameters: dict
+
+ :param alias_warnings:
+ :type alias_warnings: list
+
+ :param alias_deprecations:
+ :type alias_deprecations: list
+ """
+
+ aliases_results = {} # alias:canon
+
+ for (k, v) in argument_spec.items():
+ aliases = v.get('aliases', None)
+ default = v.get('default', None)
+ required = v.get('required', False)
+
+ if alias_deprecations is not None:
+ for alias in argument_spec[k].get('deprecated_aliases', []):
+ if alias.get('name') in parameters:
+ alias_deprecations.append(alias)
+
+ if default is not None and required:
+ # not alias specific but this is a good place to check this
+ raise ValueError("internal error: required and default are mutually exclusive for %s" % k)
+
+ if aliases is None:
+ continue
+
+ if not is_iterable(aliases) or isinstance(aliases, (binary_type, text_type)):
+ raise TypeError('internal error: aliases must be a list or tuple')
+
+ for alias in aliases:
+ aliases_results[alias] = k
+ if alias in parameters:
+ if k in parameters and alias_warnings is not None:
+ alias_warnings.append((k, alias))
+ parameters[k] = parameters[alias]
+
+ return aliases_results
+
+
+def _list_deprecations(argument_spec, parameters, prefix=''):
+ """Return a list of deprecations
+
+ :arg argument_spec: An argument spec dictionary
+ :arg parameters: Dictionary of parameters
+
+ :returns: List of dictionaries containing a message and version in which
+ the deprecated parameter will be removed, or an empty list::
+
+ [{'msg': "Param 'deptest' is deprecated. See the module docs for more information", 'version': '2.9'}]
+ """
+
+ deprecations = []
+ for arg_name, arg_opts in argument_spec.items():
+ if arg_name in parameters:
+ if prefix:
+ sub_prefix = '%s["%s"]' % (prefix, arg_name)
+ else:
+ sub_prefix = arg_name
+ if arg_opts.get('removed_at_date') is not None:
+ deprecations.append({
+ 'msg': "Param '%s' is deprecated. See the module docs for more information" % sub_prefix,
+ 'date': arg_opts.get('removed_at_date'),
+ 'collection_name': arg_opts.get('removed_from_collection'),
+ })
+ elif arg_opts.get('removed_in_version') is not None:
+ deprecations.append({
+ 'msg': "Param '%s' is deprecated. See the module docs for more information" % sub_prefix,
+ 'version': arg_opts.get('removed_in_version'),
+ 'collection_name': arg_opts.get('removed_from_collection'),
+ })
+ # Check sub-argument spec
+ sub_argument_spec = arg_opts.get('options')
+ if sub_argument_spec is not None:
+ sub_arguments = parameters[arg_name]
+ if isinstance(sub_arguments, Mapping):
+ sub_arguments = [sub_arguments]
+ if isinstance(sub_arguments, list):
+ for sub_params in sub_arguments:
+ if isinstance(sub_params, Mapping):
+ deprecations.extend(_list_deprecations(sub_argument_spec, sub_params, prefix=sub_prefix))
+
+ return deprecations
+
+
+def _list_no_log_values(argument_spec, params):
+ """Return set of no log values
+
+ :arg argument_spec: An argument spec dictionary
+ :arg params: Dictionary of all parameters
+
+ :returns: Set of strings that should be hidden from output::
+
+ {'secret_dict_value', 'secret_list_item_one', 'secret_list_item_two', 'secret_string'}
+ """
+
+ no_log_values = set()
+ for arg_name, arg_opts in argument_spec.items():
+ if arg_opts.get('no_log', False):
+ # Find the value for the no_log'd param
+ no_log_object = params.get(arg_name, None)
+
+ if no_log_object:
+ try:
+ no_log_values.update(_return_datastructure_name(no_log_object))
+ except TypeError as e:
+ raise TypeError('Failed to convert "%s": %s' % (arg_name, to_native(e)))
+
+ # Get no_log values from suboptions
+ sub_argument_spec = arg_opts.get('options')
+ if sub_argument_spec is not None:
+ wanted_type = arg_opts.get('type')
+ sub_parameters = params.get(arg_name)
+
+ if sub_parameters is not None:
+ if wanted_type == 'dict' or (wanted_type == 'list' and arg_opts.get('elements', '') == 'dict'):
+ # Sub parameters can be a dict or list of dicts. Ensure parameters are always a list.
+ if not isinstance(sub_parameters, list):
+ sub_parameters = [sub_parameters]
+
+ for sub_param in sub_parameters:
+ # Validate dict fields in case they came in as strings
+
+ if isinstance(sub_param, string_types):
+ sub_param = check_type_dict(sub_param)
+
+ if not isinstance(sub_param, Mapping):
+ raise TypeError("Value '{1}' in the sub parameter field '{0}' must by a {2}, "
+ "not '{1.__class__.__name__}'".format(arg_name, sub_param, wanted_type))
+
+ no_log_values.update(_list_no_log_values(sub_argument_spec, sub_param))
+
+ return no_log_values
def _return_datastructure_name(obj):
@@ -217,79 +453,7 @@ def _remove_values_conditions(value, no_log_strings, deferred_removals):
return value
-def _sanitize_keys_conditions(value, no_log_strings, ignore_keys, deferred_removals):
- """ Helper method to sanitize_keys() to build deferred_removals and avoid deep recursion. """
- if isinstance(value, (text_type, binary_type)):
- return value
-
- if isinstance(value, Sequence):
- if isinstance(value, MutableSequence):
- new_value = type(value)()
- else:
- new_value = [] # Need a mutable value
- deferred_removals.append((value, new_value))
- return new_value
-
- if isinstance(value, Set):
- if isinstance(value, MutableSet):
- new_value = type(value)()
- else:
- new_value = set() # Need a mutable value
- deferred_removals.append((value, new_value))
- return new_value
-
- if isinstance(value, Mapping):
- if isinstance(value, MutableMapping):
- new_value = type(value)()
- else:
- new_value = {} # Need a mutable value
- deferred_removals.append((value, new_value))
- return new_value
-
- if isinstance(value, tuple(chain(integer_types, (float, bool, NoneType)))):
- return value
-
- if isinstance(value, (datetime.datetime, datetime.date)):
- return value
-
- raise TypeError('Value of unknown type: %s, %s' % (type(value), value))
-
-
-def env_fallback(*args, **kwargs):
- """Load value from environment variable"""
-
- for arg in args:
- if arg in os.environ:
- return os.environ[arg]
- raise AnsibleFallbackNotFound
-
-
-def set_fallbacks(argument_spec, parameters):
- no_log_values = set()
- for param, value in argument_spec.items():
- fallback = value.get('fallback', (None,))
- fallback_strategy = fallback[0]
- fallback_args = []
- fallback_kwargs = {}
- if param not in parameters and fallback_strategy is not None:
- for item in fallback[1:]:
- if isinstance(item, dict):
- fallback_kwargs = item
- else:
- fallback_args = item
- try:
- fallback_value = fallback_strategy(*fallback_args, **fallback_kwargs)
- except AnsibleFallbackNotFound:
- continue
- else:
- if value.get('no_log', False) and fallback_value:
- no_log_values.add(fallback_value)
- parameters[param] = fallback_value
-
- return no_log_values
-
-
-def set_defaults(argument_spec, parameters, set_default=True):
+def _set_defaults(argument_spec, parameters, set_default=True):
"""Set default values for parameters when no value is supplied.
Modifies parameters directly.
@@ -326,284 +490,50 @@ def set_defaults(argument_spec, parameters, set_default=True):
return no_log_values
-def list_no_log_values(argument_spec, params):
- """Return set of no log values
-
- :arg argument_spec: An argument spec dictionary from a module
- :arg params: Dictionary of all parameters
-
- :returns: Set of strings that should be hidden from output::
-
- {'secret_dict_value', 'secret_list_item_one', 'secret_list_item_two', 'secret_string'}
- """
-
- no_log_values = set()
- for arg_name, arg_opts in argument_spec.items():
- if arg_opts.get('no_log', False):
- # Find the value for the no_log'd param
- no_log_object = params.get(arg_name, None)
-
- if no_log_object:
- try:
- no_log_values.update(_return_datastructure_name(no_log_object))
- except TypeError as e:
- raise TypeError('Failed to convert "%s": %s' % (arg_name, to_native(e)))
-
- # Get no_log values from suboptions
- sub_argument_spec = arg_opts.get('options')
- if sub_argument_spec is not None:
- wanted_type = arg_opts.get('type')
- sub_parameters = params.get(arg_name)
-
- if sub_parameters is not None:
- if wanted_type == 'dict' or (wanted_type == 'list' and arg_opts.get('elements', '') == 'dict'):
- # Sub parameters can be a dict or list of dicts. Ensure parameters are always a list.
- if not isinstance(sub_parameters, list):
- sub_parameters = [sub_parameters]
-
- for sub_param in sub_parameters:
- # Validate dict fields in case they came in as strings
-
- if isinstance(sub_param, string_types):
- sub_param = check_type_dict(sub_param)
-
- if not isinstance(sub_param, Mapping):
- raise TypeError("Value '{1}' in the sub parameter field '{0}' must by a {2}, "
- "not '{1.__class__.__name__}'".format(arg_name, sub_param, wanted_type))
-
- no_log_values.update(list_no_log_values(sub_argument_spec, sub_param))
-
- return no_log_values
-
-
-def list_deprecations(argument_spec, parameters, prefix=''):
- """Return a list of deprecations
-
- :arg argument_spec: An argument spec dictionary from a module
- :arg parameters: Dictionary of parameters
-
- :returns: List of dictionaries containing a message and version in which
- the deprecated parameter will be removed, or an empty list::
-
- [{'msg': "Param 'deptest' is deprecated. See the module docs for more information", 'version': '2.9'}]
- """
-
- deprecations = []
- for arg_name, arg_opts in argument_spec.items():
- if arg_name in parameters:
- if prefix:
- sub_prefix = '%s["%s"]' % (prefix, arg_name)
- else:
- sub_prefix = arg_name
- if arg_opts.get('removed_at_date') is not None:
- deprecations.append({
- 'msg': "Param '%s' is deprecated. See the module docs for more information" % sub_prefix,
- 'date': arg_opts.get('removed_at_date'),
- 'collection_name': arg_opts.get('removed_from_collection'),
- })
- elif arg_opts.get('removed_in_version') is not None:
- deprecations.append({
- 'msg': "Param '%s' is deprecated. See the module docs for more information" % sub_prefix,
- 'version': arg_opts.get('removed_in_version'),
- 'collection_name': arg_opts.get('removed_from_collection'),
- })
- # Check sub-argument spec
- sub_argument_spec = arg_opts.get('options')
- if sub_argument_spec is not None:
- sub_arguments = parameters[arg_name]
- if isinstance(sub_arguments, Mapping):
- sub_arguments = [sub_arguments]
- if isinstance(sub_arguments, list):
- for sub_params in sub_arguments:
- if isinstance(sub_params, Mapping):
- deprecations.extend(list_deprecations(sub_argument_spec, sub_params, prefix=sub_prefix))
-
- return deprecations
-
-
-def sanitize_keys(obj, no_log_strings, ignore_keys=frozenset()):
- """ Sanitize the keys in a container object by removing no_log values from key names.
-
- This is a companion function to the `remove_values()` function. Similar to that function,
- we make use of deferred_removals to avoid hitting maximum recursion depth in cases of
- large data structures.
-
- :param obj: The container object to sanitize. Non-container objects are returned unmodified.
- :param no_log_strings: A set of string values we do not want logged.
- :param ignore_keys: A set of string values of keys to not sanitize.
-
- :returns: An object with sanitized keys.
- """
-
- deferred_removals = deque()
-
- no_log_strings = [to_native(s, errors='surrogate_or_strict') for s in no_log_strings]
- new_value = _sanitize_keys_conditions(obj, no_log_strings, ignore_keys, deferred_removals)
-
- while deferred_removals:
- old_data, new_data = deferred_removals.popleft()
+def _sanitize_keys_conditions(value, no_log_strings, ignore_keys, deferred_removals):
+ """ Helper method to sanitize_keys() to build deferred_removals and avoid deep recursion. """
+ if isinstance(value, (text_type, binary_type)):
+ return value
- if isinstance(new_data, Mapping):
- for old_key, old_elem in old_data.items():
- if old_key in ignore_keys or old_key.startswith('_ansible'):
- new_data[old_key] = _sanitize_keys_conditions(old_elem, no_log_strings, ignore_keys, deferred_removals)
- else:
- # Sanitize the old key. We take advantage of the sanitizing code in
- # _remove_values_conditions() rather than recreating it here.
- new_key = _remove_values_conditions(old_key, no_log_strings, None)
- new_data[new_key] = _sanitize_keys_conditions(old_elem, no_log_strings, ignore_keys, deferred_removals)
+ if isinstance(value, Sequence):
+ if isinstance(value, MutableSequence):
+ new_value = type(value)()
else:
- for elem in old_data:
- new_elem = _sanitize_keys_conditions(elem, no_log_strings, ignore_keys, deferred_removals)
- if isinstance(new_data, MutableSequence):
- new_data.append(new_elem)
- elif isinstance(new_data, MutableSet):
- new_data.add(new_elem)
- else:
- raise TypeError('Unknown container type encountered when removing private values from keys')
-
- return new_value
-
-
-def remove_values(value, no_log_strings):
- """ Remove strings in no_log_strings from value. If value is a container
- type, then remove a lot more.
-
- Use of deferred_removals exists, rather than a pure recursive solution,
- because of the potential to hit the maximum recursion depth when dealing with
- large amounts of data (see issue #24560).
- """
-
- deferred_removals = deque()
-
- no_log_strings = [to_native(s, errors='surrogate_or_strict') for s in no_log_strings]
- new_value = _remove_values_conditions(value, no_log_strings, deferred_removals)
+ new_value = [] # Need a mutable value
+ deferred_removals.append((value, new_value))
+ return new_value
- while deferred_removals:
- old_data, new_data = deferred_removals.popleft()
- if isinstance(new_data, Mapping):
- for old_key, old_elem in old_data.items():
- new_elem = _remove_values_conditions(old_elem, no_log_strings, deferred_removals)
- new_data[old_key] = new_elem
+ if isinstance(value, Set):
+ if isinstance(value, MutableSet):
+ new_value = type(value)()
else:
- for elem in old_data:
- new_elem = _remove_values_conditions(elem, no_log_strings, deferred_removals)
- if isinstance(new_data, MutableSequence):
- new_data.append(new_elem)
- elif isinstance(new_data, MutableSet):
- new_data.add(new_elem)
- else:
- raise TypeError('Unknown container type encountered when removing private values from output')
-
- return new_value
-
-
-def handle_aliases(argument_spec, parameters, alias_warnings=None, alias_deprecations=None):
- """Return a two item tuple. The first is a dictionary of aliases, the second is
- a list of legal inputs.
-
- Modify supplied parameters by adding a new key for each alias.
-
- If a list is provided to the alias_warnings parameter, it will be filled with tuples
- (option, alias) in every case where both an option and its alias are specified.
-
- If a list is provided to alias_deprecations, it will be populated with dictionaries,
- each containing deprecation information for each alias found in argument_spec.
- """
-
- legal_inputs = ['_ansible_%s' % k for k in PASS_VARS]
- aliases_results = {} # alias:canon
-
- for (k, v) in argument_spec.items():
- legal_inputs.append(k)
- aliases = v.get('aliases', None)
- default = v.get('default', None)
- required = v.get('required', False)
-
- if alias_deprecations is not None:
- for alias in argument_spec[k].get('deprecated_aliases', []):
- if alias.get('name') in parameters:
- alias_deprecations.append(alias)
-
- if default is not None and required:
- # not alias specific but this is a good place to check this
- raise ValueError("internal error: required and default are mutually exclusive for %s" % k)
-
- if aliases is None:
- continue
-
- if not is_iterable(aliases) or isinstance(aliases, (binary_type, text_type)):
- raise TypeError('internal error: aliases must be a list or tuple')
-
- for alias in aliases:
- legal_inputs.append(alias)
- aliases_results[alias] = k
- if alias in parameters:
- if k in parameters and alias_warnings is not None:
- alias_warnings.append((k, alias))
- parameters[k] = parameters[alias]
-
- return aliases_results, legal_inputs
-
-
-def get_unsupported_parameters(argument_spec, parameters, legal_inputs=None):
- """Check keys in parameters against those provided in legal_inputs
- to ensure they contain legal values. If legal_inputs are not supplied,
- they will be generated using the argument_spec.
-
- :arg argument_spec: Dictionary of parameters, their type, and valid values.
- :arg parameters: Dictionary of parameters.
- :arg legal_inputs: List of valid key names property names. Overrides values
- in argument_spec.
-
- :returns: Set of unsupported parameters. Empty set if no unsupported parameters
- are found.
- """
-
- if legal_inputs is None:
- aliases, legal_inputs = handle_aliases(argument_spec, parameters)
-
- unsupported_parameters = set()
- for k in parameters.keys():
- if k not in legal_inputs:
- unsupported_parameters.add(k)
-
- return unsupported_parameters
-
-
-def get_type_validator(wanted):
- """Returns the callable used to validate a wanted type and the type name.
-
- :arg wanted: String or callable. If a string, get the corresponding
- validation function from DEFAULT_TYPE_VALIDATORS. If callable,
- get the name of the custom callable and return that for the type_checker.
-
- :returns: Tuple of callable function or None, and a string that is the name
- of the wanted type.
- """
+ new_value = set() # Need a mutable value
+ deferred_removals.append((value, new_value))
+ return new_value
- # Use one our our builtin validators.
- if not callable(wanted):
- if wanted is None:
- # Default type for parameters
- wanted = 'str'
+ if isinstance(value, Mapping):
+ if isinstance(value, MutableMapping):
+ new_value = type(value)()
+ else:
+ new_value = {} # Need a mutable value
+ deferred_removals.append((value, new_value))
+ return new_value
- type_checker = DEFAULT_TYPE_VALIDATORS.get(wanted)
+ if isinstance(value, tuple(chain(integer_types, (float, bool, NoneType)))):
+ return value
- # Use the custom callable for validation.
- else:
- type_checker = wanted
- wanted = getattr(wanted, '__name__', to_native(type(wanted)))
+ if isinstance(value, (datetime.datetime, datetime.date)):
+ return value
- return type_checker, wanted
+ raise TypeError('Value of unknown type: %s, %s' % (type(value), value))
-def validate_elements(wanted_type, parameter, values, options_context=None, errors=None):
+def _validate_elements(wanted_type, parameter, values, options_context=None, errors=None):
if errors is None:
- errors = []
+ errors = AnsibleValidationErrorMultiple()
- type_checker, wanted_element_type = get_type_validator(wanted_type)
+ type_checker, wanted_element_type = _get_type_validator(wanted_type)
validated_parameters = []
# Get param name for strings so we can later display this value in a useful error message if needed
# Only pass 'kwargs' to our checkers and ignore custom callable checkers
@@ -622,11 +552,11 @@ def validate_elements(wanted_type, parameter, values, options_context=None, erro
if options_context:
msg += " found in '%s'" % " -> ".join(options_context)
msg += " is of type %s and we were unable to convert to %s: %s" % (type(value), wanted_element_type, to_native(e))
- errors.append(msg)
+ errors.append(ElementError(msg))
return validated_parameters
-def validate_argument_types(argument_spec, parameters, prefix='', options_context=None, errors=None):
+def _validate_argument_types(argument_spec, parameters, prefix='', options_context=None, errors=None):
"""Validate that parameter types match the type in the argument spec.
Determine the appropriate type checker function and run each
@@ -637,7 +567,7 @@ def validate_argument_types(argument_spec, parameters, prefix='', options_contex
:param argument_spec: Argument spec
:type argument_spec: dict
- :param parameters: Parameters passed to module
+ :param parameters: Parameters
:type parameters: dict
:param prefix: Name of the parent key that contains the spec. Used in the error message
@@ -653,7 +583,7 @@ def validate_argument_types(argument_spec, parameters, prefix='', options_contex
"""
if errors is None:
- errors = []
+ errors = AnsibleValidationErrorMultiple()
for param, spec in argument_spec.items():
if param not in parameters:
@@ -664,7 +594,7 @@ def validate_argument_types(argument_spec, parameters, prefix='', options_contex
continue
wanted_type = spec.get('type')
- type_checker, wanted_name = get_type_validator(wanted_type)
+ type_checker, wanted_name = _get_type_validator(wanted_type)
# Get param name for strings so we can later display this value in a useful error message if needed
# Only pass 'kwargs' to our checkers and ignore custom callable checkers
kwargs = {}
@@ -685,22 +615,22 @@ def validate_argument_types(argument_spec, parameters, prefix='', options_contex
if options_context:
msg += " found in '%s'." % " -> ".join(options_context)
msg += ", elements value check is supported only with 'list' type"
- errors.append(msg)
- parameters[param] = validate_elements(elements_wanted_type, param, elements, options_context, errors)
+ errors.append(ArgumentTypeError(msg))
+ parameters[param] = _validate_elements(elements_wanted_type, param, elements, options_context, errors)
except (TypeError, ValueError) as e:
msg = "argument '%s' is of type %s" % (param, type(value))
if options_context:
msg += " found in '%s'." % " -> ".join(options_context)
msg += " and we were unable to convert to %s: %s" % (wanted_name, to_native(e))
- errors.append(msg)
+ errors.append(ArgumentTypeError(msg))
-def validate_argument_values(argument_spec, parameters, options_context=None, errors=None):
+def _validate_argument_values(argument_spec, parameters, options_context=None, errors=None):
"""Ensure all arguments have the requested values, and there are no stray arguments"""
if errors is None:
- errors = []
+ errors = AnsibleValidationErrorMultiple()
for param, spec in argument_spec.items():
choices = spec.get('choices')
@@ -716,8 +646,8 @@ def validate_argument_values(argument_spec, parameters, options_context=None, er
choices_str = ", ".join([to_native(c) for c in choices])
msg = "value of %s must be one or more of: %s. Got no match for: %s" % (param, choices_str, diff_list)
if options_context:
- msg += " found in %s" % " -> ".join(options_context)
- errors.append(msg)
+ msg = "{0} found in {1}".format(msg, " -> ".join(options_context))
+ errors.append(ArgumentValueError(msg))
elif parameters[param] not in choices:
# PyYaml converts certain strings to bools. If we can unambiguously convert back, do so before checking
# the value. If we can't figure this out, module author is responsible.
@@ -740,23 +670,23 @@ def validate_argument_values(argument_spec, parameters, options_context=None, er
choices_str = ", ".join([to_native(c) for c in choices])
msg = "value of %s must be one of: %s, got: %s" % (param, choices_str, parameters[param])
if options_context:
- msg += " found in %s" % " -> ".join(options_context)
- errors.append(msg)
+ msg = "{0} found in {1}".format(msg, " -> ".join(options_context))
+ errors.append(ArgumentValueError(msg))
else:
msg = "internal error: choices for argument %s are not iterable: %s" % (param, choices)
if options_context:
- msg += " found in %s" % " -> ".join(options_context)
- errors.append(msg)
+ msg = "{0} found in {1}".format(msg, " -> ".join(options_context))
+ errors.append(ArgumentTypeError(msg))
-def validate_sub_spec(argument_spec, parameters, prefix='', options_context=None, errors=None, no_log_values=None, unsupported_parameters=None):
+def _validate_sub_spec(argument_spec, parameters, prefix='', options_context=None, errors=None, no_log_values=None, unsupported_parameters=None):
"""Validate sub argument spec. This function is recursive."""
if options_context is None:
options_context = []
if errors is None:
- errors = []
+ errors = AnsibleValidationErrorMultiple()
if no_log_values is None:
no_log_values = set()
@@ -766,11 +696,11 @@ def validate_sub_spec(argument_spec, parameters, prefix='', options_context=None
for param, value in argument_spec.items():
wanted = value.get('type')
- if wanted == 'dict' or (wanted == 'list' and value.get('elements', '') == dict):
+ if wanted == 'dict' or (wanted == 'list' and value.get('elements', '') == 'dict'):
sub_spec = value.get('options')
if value.get('apply_defaults', False):
if sub_spec is not None:
- if parameters.get(value) is None:
+ if parameters.get(param) is None:
parameters[param] = {}
else:
continue
@@ -788,7 +718,7 @@ def validate_sub_spec(argument_spec, parameters, prefix='', options_context=None
for idx, sub_parameters in enumerate(elements):
if not isinstance(sub_parameters, dict):
- errors.append("value of '%s' must be of type dict or list of dicts" % param)
+ errors.append(SubParameterTypeError("value of '%s' must be of type dict or list of dicts" % param))
# Set prefix for warning messages
new_prefix = prefix + param
@@ -799,53 +729,159 @@ def validate_sub_spec(argument_spec, parameters, prefix='', options_context=None
no_log_values.update(set_fallbacks(sub_spec, sub_parameters))
alias_warnings = []
+ alias_deprecations = []
try:
- options_aliases, legal_inputs = handle_aliases(sub_spec, sub_parameters, alias_warnings)
+ options_aliases = _handle_aliases(sub_spec, sub_parameters, alias_warnings, alias_deprecations)
except (TypeError, ValueError) as e:
options_aliases = {}
- legal_inputs = None
- errors.append(to_native(e))
+ errors.append(AliasError(to_native(e)))
for option, alias in alias_warnings:
warn('Both option %s and its alias %s are set.' % (option, alias))
- no_log_values.update(list_no_log_values(sub_spec, sub_parameters))
+ try:
+ no_log_values.update(_list_no_log_values(sub_spec, sub_parameters))
+ except TypeError as te:
+ errors.append(NoLogError(to_native(te)))
- if legal_inputs is None:
- legal_inputs = list(options_aliases.keys()) + list(sub_spec.keys())
- unsupported_parameters.update(get_unsupported_parameters(sub_spec, sub_parameters, legal_inputs))
+ legal_inputs = _get_legal_inputs(sub_spec, sub_parameters, options_aliases)
+ unsupported_parameters.update(_get_unsupported_parameters(sub_spec, sub_parameters, legal_inputs, options_context))
try:
- check_mutually_exclusive(value.get('mutually_exclusive'), sub_parameters)
+ check_mutually_exclusive(value.get('mutually_exclusive'), sub_parameters, options_context)
except TypeError as e:
- errors.append(to_native(e))
+ errors.append(MutuallyExclusiveError(to_native(e)))
- no_log_values.update(set_defaults(sub_spec, sub_parameters, False))
+ no_log_values.update(_set_defaults(sub_spec, sub_parameters, False))
try:
- check_required_arguments(sub_spec, sub_parameters)
+ check_required_arguments(sub_spec, sub_parameters, options_context)
except TypeError as e:
- errors.append(to_native(e))
+ errors.append(RequiredError(to_native(e)))
- validate_argument_types(sub_spec, sub_parameters, new_prefix, options_context, errors=errors)
- validate_argument_values(sub_spec, sub_parameters, options_context, errors=errors)
+ _validate_argument_types(sub_spec, sub_parameters, new_prefix, options_context, errors=errors)
+ _validate_argument_values(sub_spec, sub_parameters, options_context, errors=errors)
- checks = [
- (check_required_together, 'required_together'),
- (check_required_one_of, 'required_one_of'),
- (check_required_if, 'required_if'),
- (check_required_by, 'required_by'),
- ]
-
- for check in checks:
+ for check in _ADDITIONAL_CHECKS:
try:
- check[0](value.get(check[1]), parameters)
+ check['func'](value.get(check['attr']), sub_parameters, options_context)
except TypeError as e:
- errors.append(to_native(e))
+ errors.append(check['err'](to_native(e)))
- no_log_values.update(set_defaults(sub_spec, sub_parameters))
+ no_log_values.update(_set_defaults(sub_spec, sub_parameters))
# Handle nested specs
- validate_sub_spec(sub_spec, sub_parameters, new_prefix, options_context, errors, no_log_values, unsupported_parameters)
+ _validate_sub_spec(sub_spec, sub_parameters, new_prefix, options_context, errors, no_log_values, unsupported_parameters)
options_context.pop()
+
+
+def env_fallback(*args, **kwargs):
+ """Load value from environment variable"""
+
+ for arg in args:
+ if arg in os.environ:
+ return os.environ[arg]
+ raise AnsibleFallbackNotFound
+
+
+def set_fallbacks(argument_spec, parameters):
+ no_log_values = set()
+ for param, value in argument_spec.items():
+ fallback = value.get('fallback', (None,))
+ fallback_strategy = fallback[0]
+ fallback_args = []
+ fallback_kwargs = {}
+ if param not in parameters and fallback_strategy is not None:
+ for item in fallback[1:]:
+ if isinstance(item, dict):
+ fallback_kwargs = item
+ else:
+ fallback_args = item
+ try:
+ fallback_value = fallback_strategy(*fallback_args, **fallback_kwargs)
+ except AnsibleFallbackNotFound:
+ continue
+ else:
+ if value.get('no_log', False) and fallback_value:
+ no_log_values.add(fallback_value)
+ parameters[param] = fallback_value
+
+ return no_log_values
+
+
+def sanitize_keys(obj, no_log_strings, ignore_keys=frozenset()):
+ """ Sanitize the keys in a container object by removing no_log values from key names.
+
+ This is a companion function to the `remove_values()` function. Similar to that function,
+ we make use of deferred_removals to avoid hitting maximum recursion depth in cases of
+ large data structures.
+
+ :param obj: The container object to sanitize. Non-container objects are returned unmodified.
+ :param no_log_strings: A set of string values we do not want logged.
+ :param ignore_keys: A set of string values of keys to not sanitize.
+
+ :returns: An object with sanitized keys.
+ """
+
+ deferred_removals = deque()
+
+ no_log_strings = [to_native(s, errors='surrogate_or_strict') for s in no_log_strings]
+ new_value = _sanitize_keys_conditions(obj, no_log_strings, ignore_keys, deferred_removals)
+
+ while deferred_removals:
+ old_data, new_data = deferred_removals.popleft()
+
+ if isinstance(new_data, Mapping):
+ for old_key, old_elem in old_data.items():
+ if old_key in ignore_keys or old_key.startswith('_ansible'):
+ new_data[old_key] = _sanitize_keys_conditions(old_elem, no_log_strings, ignore_keys, deferred_removals)
+ else:
+ # Sanitize the old key. We take advantage of the sanitizing code in
+ # _remove_values_conditions() rather than recreating it here.
+ new_key = _remove_values_conditions(old_key, no_log_strings, None)
+ new_data[new_key] = _sanitize_keys_conditions(old_elem, no_log_strings, ignore_keys, deferred_removals)
+ else:
+ for elem in old_data:
+ new_elem = _sanitize_keys_conditions(elem, no_log_strings, ignore_keys, deferred_removals)
+ if isinstance(new_data, MutableSequence):
+ new_data.append(new_elem)
+ elif isinstance(new_data, MutableSet):
+ new_data.add(new_elem)
+ else:
+ raise TypeError('Unknown container type encountered when removing private values from keys')
+
+ return new_value
+
+
+def remove_values(value, no_log_strings):
+ """ Remove strings in no_log_strings from value. If value is a container
+ type, then remove a lot more.
+
+ Use of deferred_removals exists, rather than a pure recursive solution,
+ because of the potential to hit the maximum recursion depth when dealing with
+ large amounts of data (see issue #24560).
+ """
+
+ deferred_removals = deque()
+
+ no_log_strings = [to_native(s, errors='surrogate_or_strict') for s in no_log_strings]
+ new_value = _remove_values_conditions(value, no_log_strings, deferred_removals)
+
+ while deferred_removals:
+ old_data, new_data = deferred_removals.popleft()
+ if isinstance(new_data, Mapping):
+ for old_key, old_elem in old_data.items():
+ new_elem = _remove_values_conditions(old_elem, no_log_strings, deferred_removals)
+ new_data[old_key] = new_elem
+ else:
+ for elem in old_data:
+ new_elem = _remove_values_conditions(elem, no_log_strings, deferred_removals)
+ if isinstance(new_data, MutableSequence):
+ new_data.append(new_elem)
+ elif isinstance(new_data, MutableSet):
+ new_data.add(new_elem)
+ else:
+ raise TypeError('Unknown container type encountered when removing private values from output')
+
+ return new_value