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']
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
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 == []
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
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 == []
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
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
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 == []
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 == []
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
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 == []
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
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]
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()
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)
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 == []