From 6db6edfc4f49cec9ab3bb93dc900cd5ceca0e8cb Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Fri, 5 Aug 2016 06:49:34 -0700 Subject: [PATCH] YAML treats some unquoted strings as booleans. For instance, (#16961) uri: follow_redirects: no Will lead yaml to set follow_redirects=False. This is problematic when the module parameter is not a boolean value but a string. For instance: follow_redirects = dict(required=False, default='safe', choices=['all', 'safe', 'none', 'yes', 'no']), Our parameter validation code ends up getting follow_redirects="False" instead of "no". The 100% fix is for the user to quote their strings in playbooks like: uri: follow_redirects: "no" But we can fix quite a few common cases by trying to switch "False" back into the string that it was specified as. We only do this if there is only one correct choices value that could have been specified. In the follow_redirects example, a value of "True" only maps back to "yes" and a value of "False" only maps back to "no" so we can do this. If choices also contained "on" and "off" then we couldn't map back safely and would need to force the module author to change the module to handle this case. Fixes parts of the following PRs: * https://github.com/ansible/ansible-modules-core/pull/4220 * https://github.com/ansible/ansible-modules-extras/pull/2593 --- lib/ansible/module_utils/basic.py | 38 ++++++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/lib/ansible/module_utils/basic.py b/lib/ansible/module_utils/basic.py index 00dc280cf85..2650768eb85 100644 --- a/lib/ansible/module_utils/basic.py +++ b/lib/ansible/module_utils/basic.py @@ -585,6 +585,19 @@ def env_fallback(*args, **kwargs): else: raise AnsibleFallbackNotFound +def _lenient_lowercase(lst): + """Lowercase elements of a list. + + If an element is not a string, pass it through untouched. + """ + lowered = [] + for value in lst: + try: + lowered.append(value.lower()) + except AttributeError: + lowered.append(value) + return lowered + class AnsibleFallbackNotFound(Exception): pass @@ -1339,9 +1352,28 @@ class AnsibleModule(object): if isinstance(choices, SEQUENCETYPE): if k in self.params: if self.params[k] not in choices: - choices_str=",".join([str(c) for c in choices]) - msg="value of %s must be one of: %s, got: %s" % (k, choices_str, self.params[k]) - self.fail_json(msg=msg) + # 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 self.params[k] == 'False': + lowered_choices = _lenient_lowercase(choices) + FALSEY = frozenset(BOOLEANS_FALSE) + overlap = FALSEY.intersection(choices) + if len(overlap) == 1: + # Extract from a set + (self.params[k],) = overlap + + if self.params[k] == 'True': + if lowered_choices is None: + lowered_choices = _lenient_lowercase(choices) + TRUTHY = frozenset(BOOLEANS_TRUE) + overlap = TRUTHY.intersection(choices) + if len(overlap) == 1: + (self.params[k],) = overlap + + if self.params[k] not in choices: + choices_str=",".join([str(c) for c in choices]) + msg="value of %s must be one of: %s, got: %s" % (k, choices_str, self.params[k]) + self.fail_json(msg=msg) else: self.fail_json(msg="internal error: choices for argument %s are not iterable: %s" % (k, choices))