diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 16:04:21 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 16:04:21 +0000 |
commit | 8a754e0858d922e955e71b253c139e071ecec432 (patch) | |
tree | 527d16e74bfd1840c85efd675fdecad056c54107 /lib/ansible/utils/vars.py | |
parent | Initial commit. (diff) | |
download | ansible-core-upstream/2.14.3.tar.xz ansible-core-upstream/2.14.3.zip |
Adding upstream version 2.14.3.upstream/2.14.3upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
-rw-r--r-- | lib/ansible/utils/vars.py | 293 |
1 files changed, 293 insertions, 0 deletions
diff --git a/lib/ansible/utils/vars.py b/lib/ansible/utils/vars.py new file mode 100644 index 0000000..a3224c8 --- /dev/null +++ b/lib/ansible/utils/vars.py @@ -0,0 +1,293 @@ +# (c) 2012-2014, Michael DeHaan <michael.dehaan@gmail.com> +# +# This file is part of Ansible +# +# Ansible is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# Ansible is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see <http://www.gnu.org/licenses/>. + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import keyword +import random +import uuid + +from collections.abc import MutableMapping, MutableSequence +from json import dumps + +from ansible import constants as C +from ansible import context +from ansible.errors import AnsibleError, AnsibleOptionsError +from ansible.module_utils.six import string_types, PY3 +from ansible.module_utils._text import to_native, to_text +from ansible.parsing.splitter import parse_kv + + +ADDITIONAL_PY2_KEYWORDS = frozenset(("True", "False", "None")) + +_MAXSIZE = 2 ** 32 +cur_id = 0 +node_mac = ("%012x" % uuid.getnode())[:12] +random_int = ("%08x" % random.randint(0, _MAXSIZE))[:8] + + +def get_unique_id(): + global cur_id + cur_id += 1 + return "-".join([ + node_mac[0:8], + node_mac[8:12], + random_int[0:4], + random_int[4:8], + ("%012x" % cur_id)[:12], + ]) + + +def _validate_mutable_mappings(a, b): + """ + Internal convenience function to ensure arguments are MutableMappings + + This checks that all arguments are MutableMappings or raises an error + + :raises AnsibleError: if one of the arguments is not a MutableMapping + """ + + # If this becomes generally needed, change the signature to operate on + # a variable number of arguments instead. + + if not (isinstance(a, MutableMapping) and isinstance(b, MutableMapping)): + myvars = [] + for x in [a, b]: + try: + myvars.append(dumps(x)) + except Exception: + myvars.append(to_native(x)) + raise AnsibleError("failed to combine variables, expected dicts but got a '{0}' and a '{1}': \n{2}\n{3}".format( + a.__class__.__name__, b.__class__.__name__, myvars[0], myvars[1]) + ) + + +def combine_vars(a, b, merge=None): + """ + Return a copy of dictionaries of variables based on configured hash behavior + """ + + if merge or merge is None and C.DEFAULT_HASH_BEHAVIOUR == "merge": + return merge_hash(a, b) + else: + # HASH_BEHAVIOUR == 'replace' + _validate_mutable_mappings(a, b) + result = a | b + return result + + +def merge_hash(x, y, recursive=True, list_merge='replace'): + """ + Return a new dictionary result of the merges of y into x, + so that keys from y take precedence over keys from x. + (x and y aren't modified) + """ + if list_merge not in ('replace', 'keep', 'append', 'prepend', 'append_rp', 'prepend_rp'): + raise AnsibleError("merge_hash: 'list_merge' argument can only be equal to 'replace', 'keep', 'append', 'prepend', 'append_rp' or 'prepend_rp'") + + # verify x & y are dicts + _validate_mutable_mappings(x, y) + + # to speed things up: if x is empty or equal to y, return y + # (this `if` can be remove without impact on the function + # except performance) + if x == {} or x == y: + return y.copy() + + # in the following we will copy elements from y to x, but + # we don't want to modify x, so we create a copy of it + x = x.copy() + + # to speed things up: use dict.update if possible + # (this `if` can be remove without impact on the function + # except performance) + if not recursive and list_merge == 'replace': + x.update(y) + return x + + # insert each element of y in x, overriding the one in x + # (as y has higher priority) + # we copy elements from y to x instead of x to y because + # there is a high probability x will be the "default" dict the user + # want to "patch" with y + # therefore x will have much more elements than y + for key, y_value in y.items(): + # if `key` isn't in x + # update x and move on to the next element of y + if key not in x: + x[key] = y_value + continue + # from this point we know `key` is in x + + x_value = x[key] + + # if both x's element and y's element are dicts + # recursively "combine" them or override x's with y's element + # depending on the `recursive` argument + # and move on to the next element of y + if isinstance(x_value, MutableMapping) and isinstance(y_value, MutableMapping): + if recursive: + x[key] = merge_hash(x_value, y_value, recursive, list_merge) + else: + x[key] = y_value + continue + + # if both x's element and y's element are lists + # "merge" them depending on the `list_merge` argument + # and move on to the next element of y + if isinstance(x_value, MutableSequence) and isinstance(y_value, MutableSequence): + if list_merge == 'replace': + # replace x value by y's one as it has higher priority + x[key] = y_value + elif list_merge == 'append': + x[key] = x_value + y_value + elif list_merge == 'prepend': + x[key] = y_value + x_value + elif list_merge == 'append_rp': + # append all elements from y_value (high prio) to x_value (low prio) + # and remove x_value elements that are also in y_value + # we don't remove elements from x_value nor y_value that were already in double + # (we assume that there is a reason if there where such double elements) + # _rp stands for "remove present" + x[key] = [z for z in x_value if z not in y_value] + y_value + elif list_merge == 'prepend_rp': + # same as 'append_rp' but y_value elements are prepend + x[key] = y_value + [z for z in x_value if z not in y_value] + # else 'keep' + # keep x value even if y it's of higher priority + # it's done by not changing x[key] + continue + + # else just override x's element with y's one + x[key] = y_value + + return x + + +def load_extra_vars(loader): + extra_vars = {} + for extra_vars_opt in context.CLIARGS.get('extra_vars', tuple()): + data = None + extra_vars_opt = to_text(extra_vars_opt, errors='surrogate_or_strict') + if extra_vars_opt is None or not extra_vars_opt: + continue + + if extra_vars_opt.startswith(u"@"): + # Argument is a YAML file (JSON is a subset of YAML) + data = loader.load_from_file(extra_vars_opt[1:]) + elif extra_vars_opt[0] in [u'/', u'.']: + raise AnsibleOptionsError("Please prepend extra_vars filename '%s' with '@'" % extra_vars_opt) + elif extra_vars_opt[0] in [u'[', u'{']: + # Arguments as YAML + data = loader.load(extra_vars_opt) + else: + # Arguments as Key-value + data = parse_kv(extra_vars_opt) + + if isinstance(data, MutableMapping): + extra_vars = combine_vars(extra_vars, data) + else: + raise AnsibleOptionsError("Invalid extra vars data supplied. '%s' could not be made into a dictionary" % extra_vars_opt) + + return extra_vars + + +def load_options_vars(version): + + if version is None: + version = 'Unknown' + options_vars = {'ansible_version': version} + attrs = {'check': 'check_mode', + 'diff': 'diff_mode', + 'forks': 'forks', + 'inventory': 'inventory_sources', + 'skip_tags': 'skip_tags', + 'subset': 'limit', + 'tags': 'run_tags', + 'verbosity': 'verbosity'} + + for attr, alias in attrs.items(): + opt = context.CLIARGS.get(attr) + if opt is not None: + options_vars['ansible_%s' % alias] = opt + + return options_vars + + +def _isidentifier_PY3(ident): + if not isinstance(ident, string_types): + return False + + # NOTE Python 3.7 offers str.isascii() so switch over to using it once + # we stop supporting 3.5 and 3.6 on the controller + try: + # Python 2 does not allow non-ascii characters in identifiers so unify + # the behavior for Python 3 + ident.encode('ascii') + except UnicodeEncodeError: + return False + + if not ident.isidentifier(): + return False + + if keyword.iskeyword(ident): + return False + + return True + + +def _isidentifier_PY2(ident): + if not isinstance(ident, string_types): + return False + + if not ident: + return False + + if C.INVALID_VARIABLE_NAMES.search(ident): + return False + + if keyword.iskeyword(ident) or ident in ADDITIONAL_PY2_KEYWORDS: + return False + + return True + + +if PY3: + isidentifier = _isidentifier_PY3 +else: + isidentifier = _isidentifier_PY2 + + +isidentifier.__doc__ = """Determine if string is valid identifier. + +The purpose of this function is to be used to validate any variables created in +a play to be valid Python identifiers and to not conflict with Python keywords +to prevent unexpected behavior. Since Python 2 and Python 3 differ in what +a valid identifier is, this function unifies the validation so playbooks are +portable between the two. The following changes were made: + + * disallow non-ascii characters (Python 3 allows for them as opposed to Python 2) + * True, False and None are reserved keywords (these are reserved keywords + on Python 3 as opposed to Python 2) + +:arg ident: A text string of identifier to check. Note: It is callers + responsibility to convert ident to text if it is not already. + +Originally posted at http://stackoverflow.com/a/29586366 +""" |