abacf6a108
* Begin using ArgumentSpecValidator in AnsibleModule * Add check parameters to ArgumentSpecValidator Add additional parameters for specifying required and mutually exclusive parameters. Add code to the .validate() method that runs these additional checks. * Make errors related to unsupported parameters match existing behavior Update the punctuation in the message slightly to make it more readable. Add a property to ArgumentSpecValidator to hold valid parameter names. * Set default values after performining checks * FIx sanity test failure * Use correct parameters when checking sub options * Use a dict when iterating over check functions Referencing by key names makes things a bit more readable IMO. * Fix bug in comparison for sub options evaluation * Add options_context to check functions This allows the parent parameter to be added the the error message if a validation error occurs in a sub option. * Fix bug in apply_defaults behavior of sub spec validation * Accept options_conext in get_unsupported_parameters() If options_context is supplied, a tuple of parent key names of unsupported parameter will be created. This allows the full "path" to the unsupported parameter to be reported. * Build path to the unsupported parameter for error messages. * Remove unused import * Update recursive finder test * Skip if running in check mode This was done in the _check_arguments() method. That was moved to a function that has no way of calling fail_json(), so it must be done outside of validation. This is a silght change in behavior, but I believe the correct one. Previously, only unsupported parameters would cause a failure. All other checks would not be executed if the modlue did not support check mode. This would hide validation failures in check mode. * The great purge Remove all methods related to argument spec validation from AnsibleModule * Keep _name and kind in the caller and out of the validator This seems a bit awkward since this means the caller could end up with {name} and {kind} in the error message if they don't run the messages through the .format() method with name and kind parameters. * Double moustaches work I wasn't sure if they get stripped or not. Looks like they do. Neat trick. * Add changelog * Update unsupported parameter test The error message changed to include name and kind. * Remove unused import * Add better documentation for ArgumentSpecValidator class * Fix example * Few more docs fixes * Mark required and mutually exclusive attributes as private * Mark validate functions as private * Reorganize functions in validation.py * Remove unused imports in basic.py related to argument spec validation * Create errors is module_utils We have errors in lib/ansible/errors/ but those cannot be used by modules. * Update recursive finder test * Move errors to file rather than __init__.py * Change ArgumentSpecValidator.validate() interface Raise AnsibleValidationErrorMultiple on validation error which contains all AnsibleValidationError exceptions for validation failures. Return the validated parameters if validation is successful rather than True/False. Update docs and tests. * Get attribute in loop so that the attribute name can also be used as a parameter * Shorten line * Update calling code in AnsibleModule for new validator interface * Update calling code in validate_argument_spec based in new validation interface * Base custom exception class off of Exception * Call the __init__ method of the base Exception class to populate args * Ensure no_log values are always updated * Make custom exceptions more hierarchical This redefines AnsibleError from lib/ansible/errors with a different signature since that cannot be used by modules. This may be a bad idea. Maybe lib/ansible/errors should be moved to module_utils, or AnsibleError defined in this commit should use the same signature as the original. * Just go back to basing off Exception * Return ValidationResult object on successful validation Create a ValidationResult class. Return a ValidationResult from ArgumentSpecValidator.validate() when validation is successful. Update class and method docs. Update unit tests based on interface change. * Make it easier to get error objects from AnsibleValidationResultMultiple This makes the interface cleaner when getting individual error objects contained in a single AnsibleValidationResultMultiple instance. * Define custom exception for each type of validation failure These errors indicate where a validation error occured. Currently they are empty but could contain specific data for each exception type in the future. * Update tests based on (yet another) interface change * Mark several more functions as private These are all doing rather "internal" things. The ArgumentSpecValidator class is the preferred public interface. * Move warnings and deprecations to result object Rather than calling deprecate() and warn() directly, store them on the result object so the caller can decide what to do with them. * Use subclass for module arg spec validation The subclass uses global warning and deprecations feature * Fix up docs * Remove legal_inputs munging from _handle_aliases() This is done in AnsibleModule by the _set_internal_properties() method. It only makes sense to do that for an AnsibleModule instance (it should update the parameters before performing validation) and shouldn't be done by the validator. Create a private function just for getting legal inputs since that is done in a couple of places. It may make sense store that on the ValidationResult object. * Increase test coverage * Remove unnecessary conditional ci_complete * Mark warnings and deprecations as private in the ValidationResult They can be made public once we come up with a way to make them more generally useful, probably by creating cusom objects to store the data in more structure way. * Mark valid_parameter_names as private and populate it during initialization * Use a global for storing the list of additonal checks to perform This list is used by the main validate method as well as the sub spec validation.
122 lines
3.5 KiB
Python
122 lines
3.5 KiB
Python
# -*- coding: utf-8 -*-
|
|
# Copyright (c) 2021 Ansible Project
|
|
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
|
|
|
|
from __future__ import absolute_import, division, print_function
|
|
__metaclass__ = type
|
|
|
|
import pytest
|
|
|
|
from ansible.module_utils.errors import AnsibleValidationError, AnsibleValidationErrorMultiple
|
|
from ansible.module_utils.common.arg_spec import ArgumentSpecValidator, ValidationResult
|
|
from ansible.module_utils.common.warnings import get_deprecation_messages, get_warning_messages
|
|
|
|
# id, argument spec, parameters, expected parameters, deprecation, warning
|
|
ALIAS_TEST_CASES = [
|
|
(
|
|
"alias",
|
|
{'path': {'aliases': ['dir', 'directory']}},
|
|
{'dir': '/tmp'},
|
|
{
|
|
'dir': '/tmp',
|
|
'path': '/tmp',
|
|
},
|
|
"",
|
|
"",
|
|
),
|
|
(
|
|
"alias-duplicate-warning",
|
|
{'path': {'aliases': ['dir', 'directory']}},
|
|
{
|
|
'dir': '/tmp',
|
|
'directory': '/tmp',
|
|
},
|
|
{
|
|
'dir': '/tmp',
|
|
'directory': '/tmp',
|
|
'path': '/tmp',
|
|
},
|
|
"",
|
|
{'alias': 'directory', 'option': 'path'},
|
|
),
|
|
(
|
|
"deprecated-alias",
|
|
{
|
|
'path': {
|
|
'aliases': ['not_yo_path'],
|
|
'deprecated_aliases': [
|
|
{
|
|
'name': 'not_yo_path',
|
|
'version': '1.7',
|
|
}
|
|
]
|
|
}
|
|
},
|
|
{'not_yo_path': '/tmp'},
|
|
{
|
|
'path': '/tmp',
|
|
'not_yo_path': '/tmp',
|
|
},
|
|
{'version': '1.7', 'date': None, 'collection_name': None, 'name': 'not_yo_path'},
|
|
"",
|
|
)
|
|
]
|
|
|
|
|
|
# id, argument spec, parameters, expected parameters, error
|
|
ALIAS_TEST_CASES_INVALID = [
|
|
(
|
|
"alias-invalid",
|
|
{'path': {'aliases': 'bad'}},
|
|
{},
|
|
{'path': None},
|
|
"internal error: aliases must be a list or tuple",
|
|
),
|
|
(
|
|
# This isn't related to aliases, but it exists in the alias handling code
|
|
"default-and-required",
|
|
{'name': {'default': 'ray', 'required': True}},
|
|
{},
|
|
{'name': 'ray'},
|
|
"internal error: required and default are mutually exclusive for name",
|
|
),
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
('arg_spec', 'parameters', 'expected', 'deprecation', 'warning'),
|
|
((i[1:]) for i in ALIAS_TEST_CASES),
|
|
ids=[i[0] for i in ALIAS_TEST_CASES]
|
|
)
|
|
def test_aliases(arg_spec, parameters, expected, deprecation, warning):
|
|
v = ArgumentSpecValidator(arg_spec)
|
|
result = v.validate(parameters)
|
|
|
|
assert isinstance(result, ValidationResult)
|
|
assert result.validated_parameters == expected
|
|
assert result.error_messages == []
|
|
|
|
if deprecation:
|
|
assert deprecation == result._deprecations[0]
|
|
else:
|
|
assert result._deprecations == []
|
|
|
|
if warning:
|
|
assert warning == result._warnings[0]
|
|
else:
|
|
assert result._warnings == []
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
('arg_spec', 'parameters', 'expected', 'error'),
|
|
((i[1:]) for i in ALIAS_TEST_CASES_INVALID),
|
|
ids=[i[0] for i in ALIAS_TEST_CASES_INVALID]
|
|
)
|
|
def test_aliases_invalid(arg_spec, parameters, expected, error):
|
|
v = ArgumentSpecValidator(arg_spec)
|
|
result = v.validate(parameters)
|
|
|
|
assert isinstance(result, ValidationResult)
|
|
assert error in result.error_messages
|
|
assert isinstance(result.errors.errors[0], AnsibleValidationError)
|
|
assert isinstance(result.errors, AnsibleValidationErrorMultiple)
|