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.
129 lines
6.9 KiB
Python
129 lines
6.9 KiB
Python
# (c) 2017, Toshio Kuratomi <tkuratomi@ansible.com>
|
|
#
|
|
# This file is part of Ansible
|
|
#
|
|
# Ansible is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
#
|
|
# Ansible is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU General Public License
|
|
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
# Make coding more python3-ish
|
|
from __future__ import (absolute_import, division, print_function)
|
|
__metaclass__ = type
|
|
|
|
import os
|
|
import pytest
|
|
import zipfile
|
|
|
|
from collections import namedtuple
|
|
from io import BytesIO
|
|
|
|
import ansible.errors
|
|
|
|
from ansible.executor.module_common import recursive_finder
|
|
|
|
|
|
# These are the modules that are brought in by module_utils/basic.py This may need to be updated
|
|
# when basic.py gains new imports
|
|
# We will remove these when we modify AnsiBallZ to store its args in a separate file instead of in
|
|
# basic.py
|
|
|
|
MODULE_UTILS_BASIC_FILES = frozenset(('ansible/__init__.py',
|
|
'ansible/module_utils/__init__.py',
|
|
'ansible/module_utils/_text.py',
|
|
'ansible/module_utils/basic.py',
|
|
'ansible/module_utils/six/__init__.py',
|
|
'ansible/module_utils/_text.py',
|
|
'ansible/module_utils/common/_collections_compat.py',
|
|
'ansible/module_utils/common/_json_compat.py',
|
|
'ansible/module_utils/common/collections.py',
|
|
'ansible/module_utils/common/parameters.py',
|
|
'ansible/module_utils/common/warnings.py',
|
|
'ansible/module_utils/parsing/convert_bool.py',
|
|
'ansible/module_utils/common/__init__.py',
|
|
'ansible/module_utils/common/file.py',
|
|
'ansible/module_utils/common/process.py',
|
|
'ansible/module_utils/common/sys_info.py',
|
|
'ansible/module_utils/common/text/__init__.py',
|
|
'ansible/module_utils/common/text/converters.py',
|
|
'ansible/module_utils/common/text/formatters.py',
|
|
'ansible/module_utils/common/validation.py',
|
|
'ansible/module_utils/common/_utils.py',
|
|
'ansible/module_utils/common/arg_spec.py',
|
|
'ansible/module_utils/compat/__init__.py',
|
|
'ansible/module_utils/compat/_selectors2.py',
|
|
'ansible/module_utils/compat/selectors.py',
|
|
'ansible/module_utils/compat/selinux.py',
|
|
'ansible/module_utils/distro/__init__.py',
|
|
'ansible/module_utils/distro/_distro.py',
|
|
'ansible/module_utils/errors.py',
|
|
'ansible/module_utils/parsing/__init__.py',
|
|
'ansible/module_utils/parsing/convert_bool.py',
|
|
'ansible/module_utils/pycompat24.py',
|
|
'ansible/module_utils/six/__init__.py',
|
|
))
|
|
|
|
ONLY_BASIC_FILE = frozenset(('ansible/module_utils/basic.py',))
|
|
|
|
ANSIBLE_LIB = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))), 'lib', 'ansible')
|
|
|
|
|
|
@pytest.fixture
|
|
def finder_containers():
|
|
FinderContainers = namedtuple('FinderContainers', ['zf'])
|
|
|
|
zipoutput = BytesIO()
|
|
zf = zipfile.ZipFile(zipoutput, mode='w', compression=zipfile.ZIP_STORED)
|
|
|
|
return FinderContainers(zf)
|
|
|
|
|
|
class TestRecursiveFinder(object):
|
|
def test_no_module_utils(self, finder_containers):
|
|
name = 'ping'
|
|
data = b'#!/usr/bin/python\nreturn \'{\"changed\": false}\''
|
|
recursive_finder(name, os.path.join(ANSIBLE_LIB, 'modules', 'system', 'ping.py'), data, *finder_containers)
|
|
assert frozenset(finder_containers.zf.namelist()) == MODULE_UTILS_BASIC_FILES
|
|
|
|
def test_module_utils_with_syntax_error(self, finder_containers):
|
|
name = 'fake_module'
|
|
data = b'#!/usr/bin/python\ndef something(:\n pass\n'
|
|
with pytest.raises(ansible.errors.AnsibleError) as exec_info:
|
|
recursive_finder(name, os.path.join(ANSIBLE_LIB, 'modules', 'system', 'fake_module.py'), data, *finder_containers)
|
|
assert 'Unable to import fake_module due to invalid syntax' in str(exec_info.value)
|
|
|
|
def test_module_utils_with_identation_error(self, finder_containers):
|
|
name = 'fake_module'
|
|
data = b'#!/usr/bin/python\n def something():\n pass\n'
|
|
with pytest.raises(ansible.errors.AnsibleError) as exec_info:
|
|
recursive_finder(name, os.path.join(ANSIBLE_LIB, 'modules', 'system', 'fake_module.py'), data, *finder_containers)
|
|
assert 'Unable to import fake_module due to unexpected indent' in str(exec_info.value)
|
|
|
|
#
|
|
# Test importing six with many permutations because it is not a normal module
|
|
#
|
|
def test_from_import_six(self, finder_containers):
|
|
name = 'ping'
|
|
data = b'#!/usr/bin/python\nfrom ansible.module_utils import six'
|
|
recursive_finder(name, os.path.join(ANSIBLE_LIB, 'modules', 'system', 'ping.py'), data, *finder_containers)
|
|
assert frozenset(finder_containers.zf.namelist()) == frozenset(('ansible/module_utils/six/__init__.py', )).union(MODULE_UTILS_BASIC_FILES)
|
|
|
|
def test_import_six(self, finder_containers):
|
|
name = 'ping'
|
|
data = b'#!/usr/bin/python\nimport ansible.module_utils.six'
|
|
recursive_finder(name, os.path.join(ANSIBLE_LIB, 'modules', 'system', 'ping.py'), data, *finder_containers)
|
|
assert frozenset(finder_containers.zf.namelist()) == frozenset(('ansible/module_utils/six/__init__.py', )).union(MODULE_UTILS_BASIC_FILES)
|
|
|
|
def test_import_six_from_many_submodules(self, finder_containers):
|
|
name = 'ping'
|
|
data = b'#!/usr/bin/python\nfrom ansible.module_utils.six.moves.urllib.parse import urlparse'
|
|
recursive_finder(name, os.path.join(ANSIBLE_LIB, 'modules', 'system', 'ping.py'), data, *finder_containers)
|
|
assert frozenset(finder_containers.zf.namelist()) == frozenset(('ansible/module_utils/six/__init__.py',)).union(MODULE_UTILS_BASIC_FILES)
|