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
This commit is contained in:
parent
8fa5e88b55
commit
6db6edfc4f
1 changed files with 35 additions and 3 deletions
|
@ -585,6 +585,19 @@ def env_fallback(*args, **kwargs):
|
||||||
else:
|
else:
|
||||||
raise AnsibleFallbackNotFound
|
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):
|
class AnsibleFallbackNotFound(Exception):
|
||||||
pass
|
pass
|
||||||
|
@ -1339,9 +1352,28 @@ class AnsibleModule(object):
|
||||||
if isinstance(choices, SEQUENCETYPE):
|
if isinstance(choices, SEQUENCETYPE):
|
||||||
if k in self.params:
|
if k in self.params:
|
||||||
if self.params[k] not in choices:
|
if self.params[k] not in choices:
|
||||||
choices_str=",".join([str(c) for c 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.
|
||||||
msg="value of %s must be one of: %s, got: %s" % (k, choices_str, self.params[k])
|
lowered_choices = None
|
||||||
self.fail_json(msg=msg)
|
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:
|
else:
|
||||||
self.fail_json(msg="internal error: choices for argument %s are not iterable: %s" % (k, choices))
|
self.fail_json(msg="internal error: choices for argument %s are not iterable: %s" % (k, choices))
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue