Esempio n. 1
0
def _validate_v1_source_info_schema(namespace, name, version,
                                    provided_arguments):
    argument_spec_data = dict(
        format_version=dict(choices=["1.0.0"]),
        download_url=dict(),
        version_url=dict(),
        server=dict(),
        signatures=dict(type=list,
                        suboptions=dict(
                            signature=dict(),
                            pubkey_fingerprint=dict(),
                            signing_service=dict(),
                            pulp_created=dict(),
                        )),
        name=dict(choices=[name]),
        namespace=dict(choices=[namespace]),
        version=dict(choices=[version]),
    )

    if not isinstance(provided_arguments, dict):
        raise AnsibleError(
            f'Invalid offline source info for {namespace}.{name}:{version}, expected a dict and got {type(provided_arguments)}'
        )
    validator = ArgumentSpecValidator(argument_spec_data)
    validation_result = validator.validate(provided_arguments)

    return validation_result.error_messages
 def validate(self):
     """The public validate method
     check for future argspec validation
     that is coming in 2.11, change the check according above
     """
     if HAS_ANSIBLE_ARG_SPEC_VALIDATOR:
         if self._schema_format == "doc":
             self._convert_doc_to_schema()
         if self._schema_conditionals is not None:
             self._schema = dict_merge(self._schema,
                                       self._schema_conditionals)
         invalid_keys = [
             k for k in self._schema.keys()
             if k not in VALID_ANSIBLEMODULE_ARGS
         ]
         if invalid_keys:
             valid = False
             errors = [
                 "Invalid schema. Invalid keys found: {ikeys}".format(
                     ikeys=",".join(invalid_keys))
             ]
             updated_data = {}
             return valid, errors, updated_data
         else:
             validator = ArgumentSpecValidator(**self._schema)
             result = validator.validate(self._data)
             valid = not bool(result.error_messages)
             return (
                 valid,
                 result.error_messages,
                 result.validated_parameters,
             )
     else:
         return self._validate()
def test_alias_deprecation():
    arg_spec = {
        'path': {
            'aliases': ['not_yo_path'],
            'deprecated_aliases': [{
                'name': 'not_yo_path',
                'version': '1.7',
            }]
        }
    }

    parameters = {
        'not_yo_path': '/tmp',
    }

    expected = {
        'path': '/tmp',
        'not_yo_path': '/tmp',
    }

    v = ArgumentSpecValidator(arg_spec, parameters)
    passed = v.validate()

    assert passed is True
    assert v.validated_parameters == expected
    assert v.error_messages == []
    assert "Alias 'not_yo_path' is deprecated." in get_deprecation_messages(
    )[0]['msg']
Esempio n. 4
0
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)
    def run(self, tmp=None, task_vars=None):
        '''
        Validate an argument specification against a provided set of data.

        The `validate_argument_spec` module expects to receive the arguments:
            - argument_spec: A dict whose keys are the valid argument names, and
                  whose values are dicts of the argument attributes (type, etc).
            - provided_arguments: A dict whose keys are the argument names, and
                  whose values are the argument value.

        :param tmp: Deprecated. Do not use.
        :param task_vars: A dict of task variables.
        :return: An action result dict, including a 'argument_errors' key with a
            list of validation errors found.
        '''
        if task_vars is None:
            task_vars = dict()

        result = super(ActionModule, self).run(tmp, task_vars)
        del tmp  # tmp no longer has any effect

        # This action can be called from anywhere, so pass in some info about what it is
        # validating args for so the error results make some sense
        result['validate_args_context'] = self._task.args.get('validate_args_context', {})

        if 'argument_spec' not in self._task.args:
            raise AnsibleError('"argument_spec" arg is required in args: %s' % self._task.args)

        # Get the task var called argument_spec. This will contain the arg spec
        # data dict (for the proper entry point for a role).
        argument_spec_data = self._task.args.get('argument_spec')

        # the values that were passed in and will be checked against argument_spec
        provided_arguments = self._task.args.get('provided_arguments', {})

        if not isinstance(argument_spec_data, dict):
            raise AnsibleError('Incorrect type for argument_spec, expected dict and got %s' % type(argument_spec_data))

        if not isinstance(provided_arguments, dict):
            raise AnsibleError('Incorrect type for provided_arguments, expected dict and got %s' % type(provided_arguments))

        args_from_vars = self.get_args_from_task_vars(argument_spec_data, task_vars)
        provided_arguments.update(args_from_vars)

        validator = ArgumentSpecValidator(argument_spec_data)
        validation_result = validator.validate(provided_arguments)

        if validation_result.error_messages:
            result['failed'] = True
            result['msg'] = 'Validation of arguments failed:\n%s' % '\n'.join(validation_result.error_messages)
            result['argument_spec_data'] = argument_spec_data
            result['argument_errors'] = validation_result.error_messages
            return result

        result['changed'] = False
        result['msg'] = 'The arg spec validation passed'

        return result
Esempio n. 6
0
def test_nested_sub_spec():
    arg_spec = {
        'type': {},
        'car': {
            'type': 'dict',
            'options': {
                'make': {},
                'model': {},
                'customizations': {
                    'type': 'dict',
                    'options': {
                        'engine': {},
                        'transmission': {},
                        'color': {},
                        'max_rpm': {
                            'type': 'int'
                        },
                    }
                }
            }
        }
    }

    parameters = {
        'type': 'endurance',
        'car': {
            'make': 'Ford',
            'model': 'GT-40',
            'customizations': {
                'engine': '7.0 L',
                'transmission': '5-speed',
                'color': 'Ford blue',
                'max_rpm': '6000',
            }
        }
    }

    expected = {
        'type': 'endurance',
        'car': {
            'make': 'Ford',
            'model': 'GT-40',
            'customizations': {
                'engine': '7.0 L',
                'transmission': '5-speed',
                'color': 'Ford blue',
                'max_rpm': 6000,
            }
        }
    }

    v = ArgumentSpecValidator(arg_spec)
    result = v.validate(parameters)

    assert isinstance(result, ValidationResult)
    assert result.validated_parameters == expected
    assert result.error_messages == []
Esempio n. 7
0
def test_invalid_spec(arg_spec, parameters, expected, error):
    v = ArgumentSpecValidator(arg_spec, parameters)
    passed = v.validate()

    if PY2:
        error = error.replace('class', 'type')

    assert error in v.error_messages[0]
    assert v.validated_parameters == expected
    assert passed is False
Esempio n. 8
0
def test_valid_spec(arg_spec, parameters, expected, mocker):

    mocker.patch('ansible.module_utils.common.validation.os.path.expanduser', return_value='/home/ansible/bin')
    mocker.patch('ansible.module_utils.common.validation.os.path.expandvars', return_value='/home/ansible/bin')

    v = ArgumentSpecValidator(arg_spec, parameters)
    passed = v.validate()

    assert v.validated_parameters == expected
    assert v.error_messages == []
    assert passed is True
def test_invalid_spec(arg_spec, parameters, expected, unsupported, error):
    v = ArgumentSpecValidator(arg_spec)
    result = v.validate(parameters)

    with pytest.raises(AnsibleValidationErrorMultiple) as exc_info:
        raise result.errors

    if PY2:
        error = error.replace('class', 'type')

    assert isinstance(result, ValidationResult)
    assert error in exc_info.value.msg
    assert error in result.error_messages[0]
    assert result.unsupported_parameters == unsupported
    assert result.validated_parameters == expected
def test_valid_spec(arg_spec, parameters, expected, valid_params, mocker):
    mocker.patch('ansible.module_utils.common.validation.os.path.expanduser', return_value='/home/ansible/bin')
    mocker.patch('ansible.module_utils.common.validation.os.path.expandvars', return_value='/home/ansible/bin')

    v = ArgumentSpecValidator(arg_spec)
    result = v.validate(parameters)

    assert isinstance(result, ValidationResult)
    assert result.validated_parameters == expected
    assert result.unsupported_parameters == set()
    assert result.error_messages == []
    assert v._valid_parameter_names == valid_params

    # Again to check caching
    assert v._valid_parameter_names == valid_params
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 == []
Esempio n. 12
0
def test_spec_with_aliases():
    arg_spec = {'path': {'aliases': ['dir', 'directory']}}

    parameters = {
        'dir': '/tmp',
        'directory': '/tmp',
    }

    expected = {
        'dir': '/tmp',
        'directory': '/tmp',
        'path': '/tmp',
    }

    v = ArgumentSpecValidator(arg_spec, parameters)
    passed = v.validate()

    assert passed is True
    assert v.validated_parameters == expected
Esempio n. 13
0
def test_required_and_default():
    arg_spec = {
        'param_req': {
            'required': True,
            'default': 'DEFAULT'
        },
    }

    v = ArgumentSpecValidator(arg_spec, {})
    passed = v.validate()

    expected = {'param_req': 'DEFAULT'}

    expected_errors = [
        'internal error: required and default are mutually exclusive for param_req',
    ]

    assert passed is False
    assert v.validated_parameters == expected
    assert v.error_messages == expected_errors
Esempio n. 14
0
def test_spec_with_defaults():
    arg_spec = {
        'param_str': {
            'type': 'str',
            'default': 'DEFAULT'
        },
    }

    parameters = {}

    expected = {
        'param_str': 'DEFAULT',
    }

    v = ArgumentSpecValidator(arg_spec, parameters)
    passed = v.validate()

    assert passed is True
    assert v.validated_parameters == expected
    assert v.error_messages == []
Esempio n. 15
0
def test_sub_spec():
    arg_spec = {
        'state': {},
        'user': {
            'type': 'dict',
            'options': {
                'first': {
                    'no_log': True
                },
                'last': {},
                'age': {
                    'type': 'int'
                },
            }
        }
    }

    parameters = {
        'state': 'present',
        'user': {
            'first': 'Rey',
            'last': 'Skywalker',
            'age': '19',
        }
    }

    expected = {
        'state': 'present',
        'user': {
            'first': 'Rey',
            'last': 'Skywalker',
            'age': 19,
        }
    }

    v = ArgumentSpecValidator(arg_spec)
    result = v.validate(parameters)

    assert isinstance(result, ValidationResult)
    assert result.validated_parameters == expected
    assert result.error_messages == []
Esempio n. 16
0
def test_spec_with_elements():
    arg_spec = {
        'param_list': {
            'type': 'list',
            'elements': 'int',
        }
    }

    parameters = {
        'param_list': [55, 33, 34, '22'],
    }

    expected = {
        'param_list': [55, 33, 34, 22],
    }

    v = ArgumentSpecValidator(arg_spec, parameters)
    passed = v.validate()

    assert passed is True
    assert v.error_messages == []
    assert v.validated_parameters == expected
Esempio n. 17
0
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 == []
    assert result._aliases == {
        alias: param
        for param, value in arg_spec.items()
        for alias in value.get("aliases", [])
    }

    if deprecation:
        assert deprecation == result._deprecations[0]
    else:
        assert result._deprecations == []

    if warning:
        assert warning == result._warnings[0]
    else:
        assert result._warnings == []
Esempio n. 18
0
def test_sub_spec():
    arg_spec = {
        'state': {},
        'user': {
            'type': 'dict',
            'options': {
                'first': {'no_log': True},
                'last': {},
                'age': {'type': 'int'},
            }
        }
    }

    parameters = {
        'state': 'present',
        'user': {
            'first': 'Rey',
            'last': 'Skywalker',
            'age': '19',
        }
    }

    expected = {
        'state': 'present',
        'user': {
            'first': 'Rey',
            'last': 'Skywalker',
            'age': 19,
        }
    }

    v = ArgumentSpecValidator(arg_spec, parameters)
    passed = v.validate()

    assert passed is True
    assert v.error_messages == []
    assert v.validated_parameters == expected
Esempio n. 19
0
def test_aliases(arg_spec, parameters, expected, passfail, error, deprecation,
                 warning):
    v = ArgumentSpecValidator(arg_spec, parameters)
    passed = v.validate()

    assert passed is passfail
    assert v.validated_parameters == expected

    if not error:
        assert v.error_messages == []
    else:
        assert error in v.error_messages[0]

    deprecations = get_deprecation_messages()
    if not deprecations:
        assert deprecations == ()
    else:
        assert deprecation in get_deprecation_messages()[0]['msg']

    warnings = get_warning_messages()
    if not warning:
        assert warnings == ()
    else:
        assert warning in warnings[0]
Esempio n. 20
0
    def __init__(self, action_plugin, argument_spec, bypass_checks=False,
                 mutually_exclusive=None, required_together=None,
                 required_one_of=None, supports_check_mode=False,
                 required_if=None, required_by=None):
        # Internal data
        self.__action_plugin = action_plugin
        self.__warnings = []
        self.__deprecations = []

        # AnsibleModule data
        self._name = self.__action_plugin._task.action
        self.argument_spec = argument_spec
        self.supports_check_mode = supports_check_mode
        self.check_mode = self.__action_plugin._play_context.check_mode
        self.bypass_checks = bypass_checks
        self.no_log = self.__action_plugin._play_context.no_log

        self.mutually_exclusive = mutually_exclusive
        self.required_together = required_together
        self.required_one_of = required_one_of
        self.required_if = required_if
        self.required_by = required_by
        self._diff = self.__action_plugin._play_context.diff
        self._verbosity = self.__action_plugin._display.verbosity
        self._string_conversion_action = C.STRING_CONVERSION_ACTION

        self.aliases = {}
        self._legal_inputs = []
        self._options_context = list()

        self.params = copy.deepcopy(action_plugin._task.args)
        self.no_log_values = set()
        if HAS_ARGSPEC_VALIDATOR:
            self._validator = ArgumentSpecValidator(
                self.argument_spec,
                self.mutually_exclusive,
                self.required_together,
                self.required_one_of,
                self.required_if,
                self.required_by,
            )
            self._validation_result = self._validator.validate(self.params)
            self.params.update(self._validation_result.validated_parameters)
            self.no_log_values.update(self._validation_result._no_log_values)

            try:
                error = self._validation_result.errors[0]
            except IndexError:
                error = None

            # We cannot use ModuleArgumentSpecValidator directly since it uses mechanisms for reporting
            # warnings and deprecations that do not work in plugins. This is a copy of that code adjusted
            # for our use-case:
            for d in self._validation_result._deprecations:
                self.deprecate(
                    "Alias '{name}' is deprecated. See the module docs for more information".format(name=d['name']),
                    version=d.get('version'), date=d.get('date'), collection_name=d.get('collection_name'))

            for w in self._validation_result._warnings:
                self.warn('Both option {option} and its alias {alias} are set.'.format(option=w['option'], alias=w['alias']))

            # Fail for validation errors, even in check mode
            if error:
                msg = self._validation_result.errors.msg
                if isinstance(error, UnsupportedError):
                    msg = "Unsupported parameters for ({name}) {kind}: {msg}".format(name=self._name, kind='module', msg=msg)

                self.fail_json(msg=msg)
        else:
            self._set_fallbacks()

            # append to legal_inputs and then possibly check against them
            try:
                self.aliases = self._handle_aliases()
            except (ValueError, TypeError) as e:
                # Use exceptions here because it isn't safe to call fail_json until no_log is processed
                raise _ModuleExitException(dict(failed=True, msg="Module alias error: %s" % to_native(e)))

            # Save parameter values that should never be logged
            self._handle_no_log_values()

            self._check_arguments()

            # check exclusive early
            if not bypass_checks:
                self._check_mutually_exclusive(mutually_exclusive)

            self._set_defaults(pre=True)

            self._CHECK_ARGUMENT_TYPES_DISPATCHER = {
                'str': self._check_type_str,
                'list': check_type_list,
                'dict': check_type_dict,
                'bool': check_type_bool,
                'int': check_type_int,
                'float': check_type_float,
                'path': check_type_path,
                'raw': check_type_raw,
                'jsonarg': check_type_jsonarg,
                'json': check_type_jsonarg,
                'bytes': check_type_bytes,
                'bits': check_type_bits,
            }
            if not bypass_checks:
                self._check_required_arguments()
                self._check_argument_types()
                self._check_argument_values()
                self._check_required_together(required_together)
                self._check_required_one_of(required_one_of)
                self._check_required_if(required_if)
                self._check_required_by(required_by)

            self._set_defaults(pre=False)

            # deal with options sub-spec
            self._handle_options()
Esempio n. 21
0
class AnsibleActionModule(object):
    def __init__(self, action_plugin, argument_spec, bypass_checks=False,
                 mutually_exclusive=None, required_together=None,
                 required_one_of=None, supports_check_mode=False,
                 required_if=None, required_by=None):
        # Internal data
        self.__action_plugin = action_plugin
        self.__warnings = []
        self.__deprecations = []

        # AnsibleModule data
        self._name = self.__action_plugin._task.action
        self.argument_spec = argument_spec
        self.supports_check_mode = supports_check_mode
        self.check_mode = self.__action_plugin._play_context.check_mode
        self.bypass_checks = bypass_checks
        self.no_log = self.__action_plugin._play_context.no_log

        self.mutually_exclusive = mutually_exclusive
        self.required_together = required_together
        self.required_one_of = required_one_of
        self.required_if = required_if
        self.required_by = required_by
        self._diff = self.__action_plugin._play_context.diff
        self._verbosity = self.__action_plugin._display.verbosity
        self._string_conversion_action = C.STRING_CONVERSION_ACTION

        self.aliases = {}
        self._legal_inputs = []
        self._options_context = list()

        self.params = copy.deepcopy(action_plugin._task.args)
        self.no_log_values = set()
        if HAS_ARGSPEC_VALIDATOR:
            self._validator = ArgumentSpecValidator(
                self.argument_spec,
                self.mutually_exclusive,
                self.required_together,
                self.required_one_of,
                self.required_if,
                self.required_by,
            )
            self._validation_result = self._validator.validate(self.params)
            self.params.update(self._validation_result.validated_parameters)
            self.no_log_values.update(self._validation_result._no_log_values)

            try:
                error = self._validation_result.errors[0]
            except IndexError:
                error = None

            # We cannot use ModuleArgumentSpecValidator directly since it uses mechanisms for reporting
            # warnings and deprecations that do not work in plugins. This is a copy of that code adjusted
            # for our use-case:
            for d in self._validation_result._deprecations:
                self.deprecate(
                    "Alias '{name}' is deprecated. See the module docs for more information".format(name=d['name']),
                    version=d.get('version'), date=d.get('date'), collection_name=d.get('collection_name'))

            for w in self._validation_result._warnings:
                self.warn('Both option {option} and its alias {alias} are set.'.format(option=w['option'], alias=w['alias']))

            # Fail for validation errors, even in check mode
            if error:
                msg = self._validation_result.errors.msg
                if isinstance(error, UnsupportedError):
                    msg = "Unsupported parameters for ({name}) {kind}: {msg}".format(name=self._name, kind='module', msg=msg)

                self.fail_json(msg=msg)
        else:
            self._set_fallbacks()

            # append to legal_inputs and then possibly check against them
            try:
                self.aliases = self._handle_aliases()
            except (ValueError, TypeError) as e:
                # Use exceptions here because it isn't safe to call fail_json until no_log is processed
                raise _ModuleExitException(dict(failed=True, msg="Module alias error: %s" % to_native(e)))

            # Save parameter values that should never be logged
            self._handle_no_log_values()

            self._check_arguments()

            # check exclusive early
            if not bypass_checks:
                self._check_mutually_exclusive(mutually_exclusive)

            self._set_defaults(pre=True)

            self._CHECK_ARGUMENT_TYPES_DISPATCHER = {
                'str': self._check_type_str,
                'list': check_type_list,
                'dict': check_type_dict,
                'bool': check_type_bool,
                'int': check_type_int,
                'float': check_type_float,
                'path': check_type_path,
                'raw': check_type_raw,
                'jsonarg': check_type_jsonarg,
                'json': check_type_jsonarg,
                'bytes': check_type_bytes,
                'bits': check_type_bits,
            }
            if not bypass_checks:
                self._check_required_arguments()
                self._check_argument_types()
                self._check_argument_values()
                self._check_required_together(required_together)
                self._check_required_one_of(required_one_of)
                self._check_required_if(required_if)
                self._check_required_by(required_by)

            self._set_defaults(pre=False)

            # deal with options sub-spec
            self._handle_options()

    def _handle_aliases(self, spec=None, param=None, option_prefix=''):
        if spec is None:
            spec = self.argument_spec
        if param is None:
            param = self.params

        # this uses exceptions as it happens before we can safely call fail_json
        alias_warnings = []
        alias_results, self._legal_inputs = handle_aliases(spec, param, alias_warnings=alias_warnings)
        for option, alias in alias_warnings:
            self.warn('Both option %s and its alias %s are set.' % (option_prefix + option, option_prefix + alias))

        deprecated_aliases = []
        for i in spec.keys():
            if 'deprecated_aliases' in spec[i].keys():
                for alias in spec[i]['deprecated_aliases']:
                    deprecated_aliases.append(alias)

        for deprecation in deprecated_aliases:
            if deprecation['name'] in param.keys():
                self.deprecate("Alias '%s' is deprecated. See the module docs for more information" % deprecation['name'],
                               version=deprecation.get('version'), date=deprecation.get('date'),
                               collection_name=deprecation.get('collection_name'))
        return alias_results

    def _handle_no_log_values(self, spec=None, param=None):
        if spec is None:
            spec = self.argument_spec
        if param is None:
            param = self.params

        try:
            self.no_log_values.update(list_no_log_values(spec, param))
        except TypeError as te:
            self.fail_json(msg="Failure when processing no_log parameters. Module invocation will be hidden. "
                               "%s" % to_native(te), invocation={'module_args': 'HIDDEN DUE TO FAILURE'})

        for message in list_deprecations(spec, param):
            self.deprecate(message['msg'], version=message.get('version'), date=message.get('date'),
                           collection_name=message.get('collection_name'))

    def _check_arguments(self, spec=None, param=None, legal_inputs=None):
        self._syslog_facility = 'LOG_USER'
        unsupported_parameters = set()
        if spec is None:
            spec = self.argument_spec
        if param is None:
            param = self.params
        if legal_inputs is None:
            legal_inputs = self._legal_inputs

        for k in list(param.keys()):

            if k not in legal_inputs:
                unsupported_parameters.add(k)

        for k in PASS_VARS:
            # handle setting internal properties from internal ansible vars
            param_key = '_ansible_%s' % k
            if param_key in param:
                if k in PASS_BOOLS:
                    setattr(self, PASS_VARS[k][0], self.boolean(param[param_key]))
                else:
                    setattr(self, PASS_VARS[k][0], param[param_key])

                # clean up internal top level params:
                if param_key in self.params:
                    del self.params[param_key]
            else:
                # use defaults if not already set
                if not hasattr(self, PASS_VARS[k][0]):
                    setattr(self, PASS_VARS[k][0], PASS_VARS[k][1])

        if unsupported_parameters:
            msg = "Unsupported parameters for (%s) module: %s" % (self._name, ', '.join(sorted(list(unsupported_parameters))))
            if self._options_context:
                msg += " found in %s." % " -> ".join(self._options_context)
            supported_parameters = list()
            for key in sorted(spec.keys()):
                if 'aliases' in spec[key] and spec[key]['aliases']:
                    supported_parameters.append("%s (%s)" % (key, ', '.join(sorted(spec[key]['aliases']))))
                else:
                    supported_parameters.append(key)
            msg += " Supported parameters include: %s" % (', '.join(supported_parameters))
            self.fail_json(msg=msg)
        if self.check_mode and not self.supports_check_mode:
            self.exit_json(skipped=True, msg="action module (%s) does not support check mode" % self._name)

    def _count_terms(self, check, param=None):
        if param is None:
            param = self.params
        return count_terms(check, param)

    def _check_mutually_exclusive(self, spec, param=None):
        if param is None:
            param = self.params

        try:
            check_mutually_exclusive(spec, param)
        except TypeError as e:
            msg = to_native(e)
            if self._options_context:
                msg += " found in %s" % " -> ".join(self._options_context)
            self.fail_json(msg=msg)

    def _check_required_one_of(self, spec, param=None):
        if spec is None:
            return

        if param is None:
            param = self.params

        try:
            check_required_one_of(spec, param)
        except TypeError as e:
            msg = to_native(e)
            if self._options_context:
                msg += " found in %s" % " -> ".join(self._options_context)
            self.fail_json(msg=msg)

    def _check_required_together(self, spec, param=None):
        if spec is None:
            return
        if param is None:
            param = self.params

        try:
            check_required_together(spec, param)
        except TypeError as e:
            msg = to_native(e)
            if self._options_context:
                msg += " found in %s" % " -> ".join(self._options_context)
            self.fail_json(msg=msg)

    def _check_required_by(self, spec, param=None):
        if spec is None:
            return
        if param is None:
            param = self.params

        try:
            check_required_by(spec, param)
        except TypeError as e:
            self.fail_json(msg=to_native(e))

    def _check_required_arguments(self, spec=None, param=None):
        if spec is None:
            spec = self.argument_spec
        if param is None:
            param = self.params

        try:
            check_required_arguments(spec, param)
        except TypeError as e:
            msg = to_native(e)
            if self._options_context:
                msg += " found in %s" % " -> ".join(self._options_context)
            self.fail_json(msg=msg)

    def _check_required_if(self, spec, param=None):
        ''' ensure that parameters which conditionally required are present '''
        if spec is None:
            return
        if param is None:
            param = self.params

        try:
            check_required_if(spec, param)
        except TypeError as e:
            msg = to_native(e)
            if self._options_context:
                msg += " found in %s" % " -> ".join(self._options_context)
            self.fail_json(msg=msg)

    def _check_argument_values(self, spec=None, param=None):
        ''' ensure all arguments have the requested values, and there are no stray arguments '''
        if spec is None:
            spec = self.argument_spec
        if param is None:
            param = self.params
        for (k, v) in spec.items():
            choices = v.get('choices', None)
            if choices is None:
                continue
            if isinstance(choices, SEQUENCETYPE) and not isinstance(choices, (binary_type, text_type)):
                if k in param:
                    # Allow one or more when type='list' param with choices
                    if isinstance(param[k], list):
                        diff_list = ", ".join([item for item in param[k] if item not in choices])
                        if diff_list:
                            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" % (k, choices_str, diff_list)
                            if self._options_context:
                                msg += " found in %s" % " -> ".join(self._options_context)
                            self.fail_json(msg=msg)
                    elif param[k] 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.
                        lowered_choices = None
                        if param[k] == 'False':
                            lowered_choices = lenient_lowercase(choices)
                            overlap = BOOLEANS_FALSE.intersection(choices)
                            if len(overlap) == 1:
                                # Extract from a set
                                (param[k],) = overlap

                        if param[k] == 'True':
                            if lowered_choices is None:
                                lowered_choices = lenient_lowercase(choices)
                            overlap = BOOLEANS_TRUE.intersection(choices)
                            if len(overlap) == 1:
                                (param[k],) = overlap

                        if param[k] not in choices:
                            choices_str = ", ".join([to_native(c) for c in choices])
                            msg = "value of %s must be one of: %s, got: %s" % (k, choices_str, param[k])
                            if self._options_context:
                                msg += " found in %s" % " -> ".join(self._options_context)
                            self.fail_json(msg=msg)
            else:
                msg = "internal error: choices for argument %s are not iterable: %s" % (k, choices)
                if self._options_context:
                    msg += " found in %s" % " -> ".join(self._options_context)
                self.fail_json(msg=msg)

    def safe_eval(self, value, locals=None, include_exceptions=False):
        return safe_eval(value, locals, include_exceptions)

    def _check_type_str(self, value, param=None, prefix=''):
        opts = {
            'error': False,
            'warn': False,
            'ignore': True
        }

        # Ignore, warn, or error when converting to a string.
        allow_conversion = opts.get(self._string_conversion_action, True)
        try:
            return check_type_str(value, allow_conversion)
        except TypeError:
            common_msg = 'quote the entire value to ensure it does not change.'
            from_msg = '{0!r}'.format(value)
            to_msg = '{0!r}'.format(to_text(value))

            if param is not None:
                if prefix:
                    param = '{0}{1}'.format(prefix, param)

                from_msg = '{0}: {1!r}'.format(param, value)
                to_msg = '{0}: {1!r}'.format(param, to_text(value))

            if self._string_conversion_action == 'error':
                msg = common_msg.capitalize()
                raise TypeError(to_native(msg))
            elif self._string_conversion_action == 'warn':
                msg = ('The value "{0}" (type {1.__class__.__name__}) was converted to "{2}" (type string). '
                       'If this does not look like what you expect, {3}').format(from_msg, value, to_msg, common_msg)
                self.warn(to_native(msg))
                return to_native(value, errors='surrogate_or_strict')

    def _handle_options(self, argument_spec=None, params=None, prefix=''):
        ''' deal with options to create sub spec '''
        if argument_spec is None:
            argument_spec = self.argument_spec
        if params is None:
            params = self.params

        for (k, v) in argument_spec.items():
            wanted = v.get('type', None)
            if wanted == 'dict' or (wanted == 'list' and v.get('elements', '') == 'dict'):
                spec = v.get('options', None)
                if v.get('apply_defaults', False):
                    if spec is not None:
                        if params.get(k) is None:
                            params[k] = {}
                    else:
                        continue
                elif spec is None or k not in params or params[k] is None:
                    continue

                self._options_context.append(k)

                if isinstance(params[k], dict):
                    elements = [params[k]]
                else:
                    elements = params[k]

                for idx, param in enumerate(elements):
                    if not isinstance(param, dict):
                        self.fail_json(msg="value of %s must be of type dict or list of dict" % k)

                    new_prefix = prefix + k
                    if wanted == 'list':
                        new_prefix += '[%d]' % idx
                    new_prefix += '.'

                    self._set_fallbacks(spec, param)
                    options_aliases = self._handle_aliases(spec, param, option_prefix=new_prefix)

                    options_legal_inputs = list(spec.keys()) + list(options_aliases.keys())

                    self._check_arguments(spec, param, options_legal_inputs)

                    # check exclusive early
                    if not self.bypass_checks:
                        self._check_mutually_exclusive(v.get('mutually_exclusive', None), param)

                    self._set_defaults(pre=True, spec=spec, param=param)

                    if not self.bypass_checks:
                        self._check_required_arguments(spec, param)
                        self._check_argument_types(spec, param, new_prefix)
                        self._check_argument_values(spec, param)

                        self._check_required_together(v.get('required_together', None), param)
                        self._check_required_one_of(v.get('required_one_of', None), param)
                        self._check_required_if(v.get('required_if', None), param)
                        self._check_required_by(v.get('required_by', None), param)

                    self._set_defaults(pre=False, spec=spec, param=param)

                    # handle multi level options (sub argspec)
                    self._handle_options(spec, param, new_prefix)
                self._options_context.pop()

    def _get_wanted_type(self, wanted, k):
        if not callable(wanted):
            if wanted is None:
                # Mostly we want to default to str.
                # For values set to None explicitly, return None instead as
                # that allows a user to unset a parameter
                wanted = 'str'
            try:
                type_checker = self._CHECK_ARGUMENT_TYPES_DISPATCHER[wanted]
            except KeyError:
                self.fail_json(msg="implementation error: unknown type %s requested for %s" % (wanted, k))
        else:
            # set the type_checker to the callable, and reset wanted to the callable's name (or type if it doesn't have one, ala MagicMock)
            type_checker = wanted
            wanted = getattr(wanted, '__name__', to_native(type(wanted)))

        return type_checker, wanted

    def _handle_elements(self, wanted, param, values):
        type_checker, wanted_name = self._get_wanted_type(wanted, param)
        validated_params = []
        # 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 = {}
        if wanted_name == 'str' and isinstance(wanted, string_types):
            if isinstance(param, string_types):
                kwargs['param'] = param
            elif isinstance(param, dict):
                kwargs['param'] = list(param.keys())[0]
        for value in values:
            try:
                validated_params.append(type_checker(value, **kwargs))
            except (TypeError, ValueError) as e:
                msg = "Elements value for option %s" % param
                if self._options_context:
                    msg += " found in '%s'" % " -> ".join(self._options_context)
                msg += " is of type %s and we were unable to convert to %s: %s" % (type(value), wanted_name, to_native(e))
                self.fail_json(msg=msg)
        return validated_params

    def _check_argument_types(self, spec=None, param=None, prefix=''):
        ''' ensure all arguments have the requested type '''

        if spec is None:
            spec = self.argument_spec
        if param is None:
            param = self.params

        for (k, v) in spec.items():
            wanted = v.get('type', None)
            if k not in param:
                continue

            value = param[k]
            if value is None:
                continue

            type_checker, wanted_name = self._get_wanted_type(wanted, k)
            # 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 = {}
            if wanted_name == 'str' and isinstance(type_checker, string_types):
                kwargs['param'] = list(param.keys())[0]

                # Get the name of the parent key if this is a nested option
                if prefix:
                    kwargs['prefix'] = prefix

            try:
                param[k] = type_checker(value, **kwargs)
                wanted_elements = v.get('elements', None)
                if wanted_elements:
                    if wanted != 'list' or not isinstance(param[k], list):
                        msg = "Invalid type %s for option '%s'" % (wanted_name, param)
                        if self._options_context:
                            msg += " found in '%s'." % " -> ".join(self._options_context)
                        msg += ", elements value check is supported only with 'list' type"
                        self.fail_json(msg=msg)
                    param[k] = self._handle_elements(wanted_elements, k, param[k])

            except (TypeError, ValueError) as e:
                msg = "argument %s is of type %s" % (k, type(value))
                if self._options_context:
                    msg += " found in '%s'." % " -> ".join(self._options_context)
                msg += " and we were unable to convert to %s: %s" % (wanted_name, to_native(e))
                self.fail_json(msg=msg)

    def _set_defaults(self, pre=True, spec=None, param=None):
        if spec is None:
            spec = self.argument_spec
        if param is None:
            param = self.params
        for (k, v) in spec.items():
            default = v.get('default', None)
            if pre is True:
                # this prevents setting defaults on required items
                if default is not None and k not in param:
                    param[k] = default
            else:
                # make sure things without a default still get set None
                if k not in param:
                    param[k] = default

    def _set_fallbacks(self, spec=None, param=None):
        if spec is None:
            spec = self.argument_spec
        if param is None:
            param = self.params

        for (k, v) in spec.items():
            fallback = v.get('fallback', (None,))
            fallback_strategy = fallback[0]
            fallback_args = []
            fallback_kwargs = {}
            if k not in param and fallback_strategy is not None:
                for item in fallback[1:]:
                    if isinstance(item, dict):
                        fallback_kwargs = item
                    else:
                        fallback_args = item
                try:
                    param[k] = fallback_strategy(*fallback_args, **fallback_kwargs)
                except AnsibleFallbackNotFound:
                    continue

    def warn(self, warning):
        # Copied from ansible.module_utils.common.warnings:
        if isinstance(warning, string_types):
            self.__warnings.append(warning)
        else:
            raise TypeError("warn requires a string not a %s" % type(warning))

    def deprecate(self, msg, version=None, date=None, collection_name=None):
        if version is not None and date is not None:
            raise AssertionError("implementation error -- version and date must not both be set")

        # Copied from ansible.module_utils.common.warnings:
        if isinstance(msg, string_types):
            # For compatibility, we accept that neither version nor date is set,
            # and treat that the same as if version would haven been set
            if date is not None:
                self.__deprecations.append({'msg': msg, 'date': date, 'collection_name': collection_name})
            else:
                self.__deprecations.append({'msg': msg, 'version': version, 'collection_name': collection_name})
        else:
            raise TypeError("deprecate requires a string not a %s" % type(msg))

    def _return_formatted(self, kwargs):
        if 'invocation' not in kwargs:
            kwargs['invocation'] = {'module_args': self.params}

        if 'warnings' in kwargs:
            if isinstance(kwargs['warnings'], list):
                for w in kwargs['warnings']:
                    self.warn(w)
            else:
                self.warn(kwargs['warnings'])

        if self.__warnings:
            kwargs['warnings'] = self.__warnings

        if 'deprecations' in kwargs:
            if isinstance(kwargs['deprecations'], list):
                for d in kwargs['deprecations']:
                    if isinstance(d, SEQUENCETYPE) and len(d) == 2:
                        self.deprecate(d[0], version=d[1])
                    elif isinstance(d, Mapping):
                        self.deprecate(d['msg'], version=d.get('version'), date=d.get('date'),
                                       collection_name=d.get('collection_name'))
                    else:
                        self.deprecate(d)  # pylint: disable=ansible-deprecated-no-version
            else:
                self.deprecate(kwargs['deprecations'])  # pylint: disable=ansible-deprecated-no-version

        if self.__deprecations:
            kwargs['deprecations'] = self.__deprecations

        kwargs = remove_values(kwargs, self.no_log_values)
        raise _ModuleExitException(kwargs)

    def exit_json(self, **kwargs):
        result = dict(kwargs)
        if 'failed' not in result:
            result['failed'] = False
        self._return_formatted(result)

    def fail_json(self, msg, **kwargs):
        result = dict(kwargs)
        result['failed'] = True
        result['msg'] = msg
        self._return_formatted(result)
Esempio n. 22
0
def test_basic_spec():
    arg_spec = {
        'param_str': {
            'type': 'str'
        },
        'param_list': {
            'type': 'list'
        },
        'param_dict': {
            'type': 'dict'
        },
        'param_bool': {
            'type': 'bool'
        },
        'param_int': {
            'type': 'int'
        },
        'param_float': {
            'type': 'float'
        },
        'param_path': {
            'type': 'path'
        },
        'param_raw': {
            'type': 'raw'
        },
        'param_bytes': {
            'type': 'bytes'
        },
        'param_bits': {
            'type': 'bits'
        },
    }

    parameters = {
        'param_str': 22,
        'param_list': 'one,two,three',
        'param_dict': 'first=star,last=lord',
        'param_bool': True,
        'param_int': 22,
        'param_float': 1.5,
        'param_path': '/tmp',
        'param_raw': 'raw',
        'param_bytes': '2K',
        'param_bits': '1Mb',
    }

    expected = {
        'param_str': '22',
        'param_list': ['one', 'two', 'three'],
        'param_dict': {
            'first': 'star',
            'last': 'lord'
        },
        'param_bool': True,
        'param_float': 1.5,
        'param_int': 22,
        'param_path': '/tmp',
        'param_raw': 'raw',
        'param_bits': 1048576,
        'param_bytes': 2048,
    }

    v = ArgumentSpecValidator(arg_spec, parameters)
    passed = v.validate()

    assert passed is True
    assert v.validated_parameters == expected
    assert v.error_messages == []