From 8a754e0858d922e955e71b253c139e071ecec432 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 28 Apr 2024 18:04:21 +0200 Subject: Adding upstream version 2.14.3. Signed-off-by: Daniel Baumann --- lib/ansible/plugins/lookup/__init__.py | 130 ++++++++ lib/ansible/plugins/lookup/config.py | 156 +++++++++ lib/ansible/plugins/lookup/csvfile.py | 181 ++++++++++ lib/ansible/plugins/lookup/dict.py | 77 +++++ lib/ansible/plugins/lookup/env.py | 79 +++++ lib/ansible/plugins/lookup/file.py | 88 +++++ lib/ansible/plugins/lookup/fileglob.py | 84 +++++ lib/ansible/plugins/lookup/first_found.py | 235 +++++++++++++ lib/ansible/plugins/lookup/indexed_items.py | 52 +++ lib/ansible/plugins/lookup/ini.py | 204 ++++++++++++ lib/ansible/plugins/lookup/inventory_hostnames.py | 53 +++ lib/ansible/plugins/lookup/items.py | 73 ++++ lib/ansible/plugins/lookup/lines.py | 62 ++++ lib/ansible/plugins/lookup/list.py | 45 +++ lib/ansible/plugins/lookup/nested.py | 85 +++++ lib/ansible/plugins/lookup/password.py | 389 ++++++++++++++++++++++ lib/ansible/plugins/lookup/pipe.py | 76 +++++ lib/ansible/plugins/lookup/random_choice.py | 53 +++ lib/ansible/plugins/lookup/sequence.py | 268 +++++++++++++++ lib/ansible/plugins/lookup/subelements.py | 169 ++++++++++ lib/ansible/plugins/lookup/template.py | 165 +++++++++ lib/ansible/plugins/lookup/together.py | 68 ++++ lib/ansible/plugins/lookup/unvault.py | 63 ++++ lib/ansible/plugins/lookup/url.py | 264 +++++++++++++++ lib/ansible/plugins/lookup/varnames.py | 79 +++++ lib/ansible/plugins/lookup/vars.py | 106 ++++++ 26 files changed, 3304 insertions(+) create mode 100644 lib/ansible/plugins/lookup/__init__.py create mode 100644 lib/ansible/plugins/lookup/config.py create mode 100644 lib/ansible/plugins/lookup/csvfile.py create mode 100644 lib/ansible/plugins/lookup/dict.py create mode 100644 lib/ansible/plugins/lookup/env.py create mode 100644 lib/ansible/plugins/lookup/file.py create mode 100644 lib/ansible/plugins/lookup/fileglob.py create mode 100644 lib/ansible/plugins/lookup/first_found.py create mode 100644 lib/ansible/plugins/lookup/indexed_items.py create mode 100644 lib/ansible/plugins/lookup/ini.py create mode 100644 lib/ansible/plugins/lookup/inventory_hostnames.py create mode 100644 lib/ansible/plugins/lookup/items.py create mode 100644 lib/ansible/plugins/lookup/lines.py create mode 100644 lib/ansible/plugins/lookup/list.py create mode 100644 lib/ansible/plugins/lookup/nested.py create mode 100644 lib/ansible/plugins/lookup/password.py create mode 100644 lib/ansible/plugins/lookup/pipe.py create mode 100644 lib/ansible/plugins/lookup/random_choice.py create mode 100644 lib/ansible/plugins/lookup/sequence.py create mode 100644 lib/ansible/plugins/lookup/subelements.py create mode 100644 lib/ansible/plugins/lookup/template.py create mode 100644 lib/ansible/plugins/lookup/together.py create mode 100644 lib/ansible/plugins/lookup/unvault.py create mode 100644 lib/ansible/plugins/lookup/url.py create mode 100644 lib/ansible/plugins/lookup/varnames.py create mode 100644 lib/ansible/plugins/lookup/vars.py (limited to 'lib/ansible/plugins/lookup') diff --git a/lib/ansible/plugins/lookup/__init__.py b/lib/ansible/plugins/lookup/__init__.py new file mode 100644 index 0000000..470f060 --- /dev/null +++ b/lib/ansible/plugins/lookup/__init__.py @@ -0,0 +1,130 @@ +# (c) 2012-2014, Michael DeHaan +# +# 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 . + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +from abc import abstractmethod + +from ansible.errors import AnsibleFileNotFound +from ansible.plugins import AnsiblePlugin +from ansible.utils.display import Display + +display = Display() + +__all__ = ['LookupBase'] + + +class LookupBase(AnsiblePlugin): + + def __init__(self, loader=None, templar=None, **kwargs): + + super(LookupBase, self).__init__() + + self._loader = loader + self._templar = templar + + # Backwards compat: self._display isn't really needed, just import the global display and use that. + self._display = display + + def get_basedir(self, variables): + if 'role_path' in variables: + return variables['role_path'] + else: + return self._loader.get_basedir() + + @staticmethod + def _flatten(terms): + ret = [] + for term in terms: + if isinstance(term, (list, tuple)): + ret.extend(term) + else: + ret.append(term) + return ret + + @staticmethod + def _combine(a, b): + results = [] + for x in a: + for y in b: + results.append(LookupBase._flatten([x, y])) + return results + + @staticmethod + def _flatten_hash_to_list(terms): + ret = [] + for key in terms: + ret.append({'key': key, 'value': terms[key]}) + return ret + + @abstractmethod + def run(self, terms, variables=None, **kwargs): + """ + When the playbook specifies a lookup, this method is run. The + arguments to the lookup become the arguments to this method. One + additional keyword argument named ``variables`` is added to the method + call. It contains the variables available to ansible at the time the + lookup is templated. For instance:: + + "{{ lookup('url', 'https://toshio.fedorapeople.org/one.txt', validate_certs=True) }}" + + would end up calling the lookup plugin named url's run method like this:: + run(['https://toshio.fedorapeople.org/one.txt'], variables=available_variables, validate_certs=True) + + Lookup plugins can be used within playbooks for looping. When this + happens, the first argument is a list containing the terms. Lookup + plugins can also be called from within playbooks to return their + values into a variable or parameter. If the user passes a string in + this case, it is converted into a list. + + Errors encountered during execution should be returned by raising + AnsibleError() with a message describing the error. + + Any strings returned by this method that could ever contain non-ascii + must be converted into python's unicode type as the strings will be run + through jinja2 which has this requirement. You can use:: + + from ansible.module_utils._text import to_text + result_string = to_text(result_string) + """ + pass + + def find_file_in_search_path(self, myvars, subdir, needle, ignore_missing=False): + ''' + Return a file (needle) in the task's expected search path. + ''' + + if 'ansible_search_path' in myvars: + paths = myvars['ansible_search_path'] + else: + paths = [self.get_basedir(myvars)] + + result = None + try: + result = self._loader.path_dwim_relative_stack(paths, subdir, needle) + except AnsibleFileNotFound: + if not ignore_missing: + self._display.warning("Unable to find '%s' in expected paths (use -vvvvv to see paths)" % needle) + + return result + + def _deprecate_inline_kv(self): + # TODO: place holder to deprecate in future version allowing for long transition period + # self._display.deprecated('Passing inline k=v values embedded in a string to this lookup. Use direct ,k=v, k2=v2 syntax instead.', version='2.18') + pass diff --git a/lib/ansible/plugins/lookup/config.py b/lib/ansible/plugins/lookup/config.py new file mode 100644 index 0000000..3e5529b --- /dev/null +++ b/lib/ansible/plugins/lookup/config.py @@ -0,0 +1,156 @@ +# (c) 2017 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = """ + name: config + author: Ansible Core Team + version_added: "2.5" + short_description: Lookup current Ansible configuration values + description: + - Retrieves the value of an Ansible configuration setting. + - You can use C(ansible-config list) to see all available settings. + options: + _terms: + description: The key(s) to look up + required: True + on_missing: + description: + - action to take if term is missing from config + - Error will raise a fatal error + - Skip will just ignore the term + - Warn will skip over it but issue a warning + default: error + type: string + choices: ['error', 'skip', 'warn'] + plugin_type: + description: the type of the plugin referenced by 'plugin_name' option. + choices: ['become', 'cache', 'callback', 'cliconf', 'connection', 'httpapi', 'inventory', 'lookup', 'netconf', 'shell', 'vars'] + type: string + version_added: '2.12' + plugin_name: + description: name of the plugin for which you want to retrieve configuration settings. + type: string + version_added: '2.12' +""" + +EXAMPLES = """ + - name: Show configured default become user + ansible.builtin.debug: msg="{{ lookup('ansible.builtin.config', 'DEFAULT_BECOME_USER')}}" + + - name: print out role paths + ansible.builtin.debug: + msg: "These are the configured role paths: {{lookup('ansible.builtin.config', 'DEFAULT_ROLES_PATH')}}" + + - name: find retry files, skip if missing that key + ansible.builtin.find: + paths: "{{lookup('ansible.builtin.config', 'RETRY_FILES_SAVE_PATH')|default(playbook_dir, True)}}" + patterns: "*.retry" + + - name: see the colors + ansible.builtin.debug: msg="{{item}}" + loop: "{{lookup('ansible.builtin.config', 'COLOR_OK', 'COLOR_CHANGED', 'COLOR_SKIP', wantlist=True)}}" + + - name: skip if bad value in var + ansible.builtin.debug: msg="{{ lookup('ansible.builtin.config', config_in_var, on_missing='skip')}}" + var: + config_in_var: UNKNOWN + + - name: show remote user and port for ssh connection + ansible.builtin.debug: msg={{q("ansible.builtin.config", "remote_user", "port", plugin_type="connection", plugin_name="ssh", on_missing='skip')}} + + - name: show remote_tmp setting for shell (sh) plugin + ansible.builtin.debug: msg={{q("ansible.builtin.config", "remote_tmp", plugin_type="shell", plugin_name="sh")}} +""" + +RETURN = """ +_raw: + description: + - value(s) of the key(s) in the config + type: raw +""" + +import ansible.plugins.loader as plugin_loader + +from ansible import constants as C +from ansible.errors import AnsibleError, AnsibleLookupError, AnsibleOptionsError +from ansible.module_utils._text import to_native +from ansible.module_utils.six import string_types +from ansible.plugins.lookup import LookupBase +from ansible.utils.sentinel import Sentinel + + +class MissingSetting(AnsibleOptionsError): + pass + + +def _get_plugin_config(pname, ptype, config, variables): + try: + # plugin creates settings on load, this is cached so not too expensive to redo + loader = getattr(plugin_loader, '%s_loader' % ptype) + p = loader.get(pname, class_only=True) + if p is None: + raise AnsibleLookupError('Unable to load %s plugin "%s"' % (ptype, pname)) + result = C.config.get_config_value(config, plugin_type=ptype, plugin_name=p._load_name, variables=variables) + except AnsibleLookupError: + raise + except AnsibleError as e: + msg = to_native(e) + if 'was not defined' in msg: + raise MissingSetting(msg, orig_exc=e) + raise e + + return result + + +def _get_global_config(config): + try: + result = getattr(C, config) + if callable(result): + raise AnsibleLookupError('Invalid setting "%s" attempted' % config) + except AttributeError as e: + raise MissingSetting(to_native(e), orig_exc=e) + + return result + + +class LookupModule(LookupBase): + + def run(self, terms, variables=None, **kwargs): + + self.set_options(var_options=variables, direct=kwargs) + + missing = self.get_option('on_missing') + ptype = self.get_option('plugin_type') + pname = self.get_option('plugin_name') + + if (ptype or pname) and not (ptype and pname): + raise AnsibleOptionsError('Both plugin_type and plugin_name are required, cannot use one without the other') + + if not isinstance(missing, string_types) or missing not in ['error', 'warn', 'skip']: + raise AnsibleOptionsError('"on_missing" must be a string and one of "error", "warn" or "skip", not %s' % missing) + + ret = [] + + for term in terms: + if not isinstance(term, string_types): + raise AnsibleOptionsError('Invalid setting identifier, "%s" is not a string, its a %s' % (term, type(term))) + + result = Sentinel + try: + if pname: + result = _get_plugin_config(pname, ptype, term, variables) + else: + result = _get_global_config(term) + except MissingSetting as e: + if missing == 'error': + raise AnsibleLookupError('Unable to find setting %s' % term, orig_exc=e) + elif missing == 'warn': + self._display.warning('Skipping, did not find setting %s' % term) + elif missing == 'skip': + pass # this is not needed, but added to have all 3 options stated + + if result is not Sentinel: + ret.append(result) + return ret diff --git a/lib/ansible/plugins/lookup/csvfile.py b/lib/ansible/plugins/lookup/csvfile.py new file mode 100644 index 0000000..5932d77 --- /dev/null +++ b/lib/ansible/plugins/lookup/csvfile.py @@ -0,0 +1,181 @@ +# (c) 2013, Jan-Piet Mens +# (c) 2017 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = r""" + name: csvfile + author: Jan-Piet Mens (@jpmens) + version_added: "1.5" + short_description: read data from a TSV or CSV file + description: + - The csvfile lookup reads the contents of a file in CSV (comma-separated value) format. + The lookup looks for the row where the first column matches keyname (which can be multiple words) + and returns the value in the C(col) column (default 1, which indexed from 0 means the second column in the file). + options: + col: + description: column to return (0 indexed). + default: "1" + default: + description: what to return if the value is not found in the file. + delimiter: + description: field separator in the file, for a tab you can specify C(TAB) or C(\t). + default: TAB + file: + description: name of the CSV/TSV file to open. + default: ansible.csv + encoding: + description: Encoding (character set) of the used CSV file. + default: utf-8 + version_added: "2.1" + notes: + - The default is for TSV files (tab delimited) not CSV (comma delimited) ... yes the name is misleading. + - As of version 2.11, the search parameter (text that must match the first column of the file) and filename parameter can be multi-word. + - For historical reasons, in the search keyname, quotes are treated + literally and cannot be used around the string unless they appear + (escaped as required) in the first column of the file you are parsing. +""" + +EXAMPLES = """ +- name: Match 'Li' on the first column, return the second column (0 based index) + ansible.builtin.debug: msg="The atomic number of Lithium is {{ lookup('ansible.builtin.csvfile', 'Li file=elements.csv delimiter=,') }}" + +- name: msg="Match 'Li' on the first column, but return the 3rd column (columns start counting after the match)" + ansible.builtin.debug: msg="The atomic mass of Lithium is {{ lookup('ansible.builtin.csvfile', 'Li file=elements.csv delimiter=, col=2') }}" + +- name: Define Values From CSV File, this reads file in one go, but you could also use col= to read each in it's own lookup. + ansible.builtin.set_fact: + loop_ip: "{{ csvline[0] }}" + int_ip: "{{ csvline[1] }}" + int_mask: "{{ csvline[2] }}" + int_name: "{{ csvline[3] }}" + local_as: "{{ csvline[4] }}" + neighbor_as: "{{ csvline[5] }}" + neigh_int_ip: "{{ csvline[6] }}" + vars: + csvline = "{{ lookup('ansible.builtin.csvfile', bgp_neighbor_ip, file='bgp_neighbors.csv', delimiter=',') }}" + delegate_to: localhost +""" + +RETURN = """ + _raw: + description: + - value(s) stored in file column + type: list + elements: str +""" + +import codecs +import csv + +from collections.abc import MutableSequence + +from ansible.errors import AnsibleError, AnsibleAssertionError +from ansible.parsing.splitter import parse_kv +from ansible.plugins.lookup import LookupBase +from ansible.module_utils.six import PY2 +from ansible.module_utils._text import to_bytes, to_native, to_text + + +class CSVRecoder: + """ + Iterator that reads an encoded stream and reencodes the input to UTF-8 + """ + def __init__(self, f, encoding='utf-8'): + self.reader = codecs.getreader(encoding)(f) + + def __iter__(self): + return self + + def __next__(self): + return next(self.reader).encode("utf-8") + + next = __next__ # For Python 2 + + +class CSVReader: + """ + A CSV reader which will iterate over lines in the CSV file "f", + which is encoded in the given encoding. + """ + + def __init__(self, f, dialect=csv.excel, encoding='utf-8', **kwds): + if PY2: + f = CSVRecoder(f, encoding) + else: + f = codecs.getreader(encoding)(f) + + self.reader = csv.reader(f, dialect=dialect, **kwds) + + def __next__(self): + row = next(self.reader) + return [to_text(s) for s in row] + + next = __next__ # For Python 2 + + def __iter__(self): + return self + + +class LookupModule(LookupBase): + + def read_csv(self, filename, key, delimiter, encoding='utf-8', dflt=None, col=1): + + try: + f = open(to_bytes(filename), 'rb') + creader = CSVReader(f, delimiter=to_native(delimiter), encoding=encoding) + + for row in creader: + if len(row) and row[0] == key: + return row[int(col)] + except Exception as e: + raise AnsibleError("csvfile: %s" % to_native(e)) + + return dflt + + def run(self, terms, variables=None, **kwargs): + + ret = [] + + self.set_options(var_options=variables, direct=kwargs) + + # populate options + paramvals = self.get_options() + + for term in terms: + kv = parse_kv(term) + + if '_raw_params' not in kv: + raise AnsibleError('Search key is required but was not found') + + key = kv['_raw_params'] + + # parameters override per term using k/v + try: + for name, value in kv.items(): + if name == '_raw_params': + continue + if name not in paramvals: + raise AnsibleAssertionError('%s is not a valid option' % name) + + self._deprecate_inline_kv() + paramvals[name] = value + + except (ValueError, AssertionError) as e: + raise AnsibleError(e) + + # default is just placeholder for real tab + if paramvals['delimiter'] == 'TAB': + paramvals['delimiter'] = "\t" + + lookupfile = self.find_file_in_search_path(variables, 'files', paramvals['file']) + var = self.read_csv(lookupfile, key, paramvals['delimiter'], paramvals['encoding'], paramvals['default'], paramvals['col']) + if var is not None: + if isinstance(var, MutableSequence): + for v in var: + ret.append(v) + else: + ret.append(var) + + return ret diff --git a/lib/ansible/plugins/lookup/dict.py b/lib/ansible/plugins/lookup/dict.py new file mode 100644 index 0000000..af9a081 --- /dev/null +++ b/lib/ansible/plugins/lookup/dict.py @@ -0,0 +1,77 @@ +# (c) 2014, Kent R. Spillner +# (c) 2017 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = """ + name: dict + version_added: "1.5" + short_description: returns key/value pair items from dictionaries + description: + - Takes dictionaries as input and returns a list with each item in the list being a dictionary with 'key' and 'value' as + keys to the previous dictionary's structure. + options: + _terms: + description: + - A list of dictionaries + required: True +""" + +EXAMPLES = """ +vars: + users: + alice: + name: Alice Appleworth + telephone: 123-456-7890 + bob: + name: Bob Bananarama + telephone: 987-654-3210 +tasks: + # with predefined vars + - name: Print phone records + ansible.builtin.debug: + msg: "User {{ item.key }} is {{ item.value.name }} ({{ item.value.telephone }})" + loop: "{{ lookup('ansible.builtin.dict', users) }}" + # with inline dictionary + - name: show dictionary + ansible.builtin.debug: + msg: "{{item.key}}: {{item.value}}" + with_dict: {a: 1, b: 2, c: 3} + # Items from loop can be used in when: statements + - name: set_fact when alice in key + ansible.builtin.set_fact: + alice_exists: true + loop: "{{ lookup('ansible.builtin.dict', users) }}" + when: "'alice' in item.key" +""" + +RETURN = """ + _list: + description: + - list of composed dictonaries with key and value + type: list +""" + +from collections.abc import Mapping + +from ansible.errors import AnsibleError +from ansible.plugins.lookup import LookupBase + + +class LookupModule(LookupBase): + + def run(self, terms, variables=None, **kwargs): + + # NOTE: can remove if with_ is removed + if not isinstance(terms, list): + terms = [terms] + + results = [] + for term in terms: + # Expect any type of Mapping, notably hostvars + if not isinstance(term, Mapping): + raise AnsibleError("with_dict expects a dict") + + results.extend(self._flatten_hash_to_list(term)) + return results diff --git a/lib/ansible/plugins/lookup/env.py b/lib/ansible/plugins/lookup/env.py new file mode 100644 index 0000000..3c37b90 --- /dev/null +++ b/lib/ansible/plugins/lookup/env.py @@ -0,0 +1,79 @@ +# (c) 2012, Jan-Piet Mens +# (c) 2017 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = """ + name: env + author: Jan-Piet Mens (@jpmens) + version_added: "0.9" + short_description: Read the value of environment variables + description: + - Allows you to query the environment variables available on the + controller when you invoked Ansible. + options: + _terms: + description: + - Environment variable or list of them to lookup the values for. + required: True + default: + description: What return when the variable is undefined + type: raw + default: '' + version_added: '2.13' + notes: + - You can pass the C(Undefined) object as C(default) to force an undefined error +""" + +EXAMPLES = """ +- name: Basic usage + ansible.builtin.debug: + msg: "'{{ lookup('ansible.builtin.env', 'HOME') }}' is the HOME environment variable." + +- name: Before 2.13, how to set default value if the variable is not defined. + This cannot distinguish between USR undefined and USR=''. + ansible.builtin.debug: + msg: "{{ lookup('ansible.builtin.env', 'USR')|default('nobody', True) }} is the user." + +- name: Example how to set default value if the variable is not defined, ignores USR='' + ansible.builtin.debug: + msg: "{{ lookup('ansible.builtin.env', 'USR', default='nobody') }} is the user." + +- name: Set default value to Undefined, if the variable is not defined + ansible.builtin.debug: + msg: "{{ lookup('ansible.builtin.env', 'USR', default=Undefined) }} is the user." + +- name: Set default value to undef(), if the variable is not defined + ansible.builtin.debug: + msg: "{{ lookup('ansible.builtin.env', 'USR', default=undef()) }} is the user." +""" + +RETURN = """ + _list: + description: + - Values from the environment variables. + type: list +""" + +from jinja2.runtime import Undefined + +from ansible.errors import AnsibleUndefinedVariable +from ansible.plugins.lookup import LookupBase +from ansible.utils import py3compat + + +class LookupModule(LookupBase): + def run(self, terms, variables, **kwargs): + + self.set_options(var_options=variables, direct=kwargs) + + ret = [] + d = self.get_option('default') + for term in terms: + var = term.split()[0] + val = py3compat.environ.get(var, d) + if isinstance(val, Undefined): + raise AnsibleUndefinedVariable('The "env" lookup, found an undefined variable: %s' % var) + ret.append(val) + return ret diff --git a/lib/ansible/plugins/lookup/file.py b/lib/ansible/plugins/lookup/file.py new file mode 100644 index 0000000..fa9191e --- /dev/null +++ b/lib/ansible/plugins/lookup/file.py @@ -0,0 +1,88 @@ +# (c) 2012, Daniel Hokka Zakrisson +# (c) 2017 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = """ + name: file + author: Daniel Hokka Zakrisson (!UNKNOWN) + version_added: "0.9" + short_description: read file contents + description: + - This lookup returns the contents from a file on the Ansible controller's file system. + options: + _terms: + description: path(s) of files to read + required: True + rstrip: + description: whether or not to remove whitespace from the ending of the looked-up file + type: bool + required: False + default: True + lstrip: + description: whether or not to remove whitespace from the beginning of the looked-up file + type: bool + required: False + default: False + notes: + - if read in variable context, the file can be interpreted as YAML if the content is valid to the parser. + - this lookup does not understand 'globbing', use the fileglob lookup instead. +""" + +EXAMPLES = """ +- ansible.builtin.debug: + msg: "the value of foo.txt is {{lookup('ansible.builtin.file', '/etc/foo.txt') }}" + +- name: display multiple file contents + ansible.builtin.debug: var=item + with_file: + - "/path/to/foo.txt" + - "bar.txt" # will be looked in files/ dir relative to play or in role + - "/path/to/biz.txt" +""" + +RETURN = """ + _raw: + description: + - content of file(s) + type: list + elements: str +""" + +from ansible.errors import AnsibleError, AnsibleParserError +from ansible.plugins.lookup import LookupBase +from ansible.module_utils._text import to_text +from ansible.utils.display import Display + +display = Display() + + +class LookupModule(LookupBase): + + def run(self, terms, variables=None, **kwargs): + + ret = [] + self.set_options(var_options=variables, direct=kwargs) + + for term in terms: + display.debug("File lookup term: %s" % term) + + # Find the file in the expected search path + lookupfile = self.find_file_in_search_path(variables, 'files', term) + display.vvvv(u"File lookup using %s as file" % lookupfile) + try: + if lookupfile: + b_contents, show_data = self._loader._get_file_contents(lookupfile) + contents = to_text(b_contents, errors='surrogate_or_strict') + if self.get_option('lstrip'): + contents = contents.lstrip() + if self.get_option('rstrip'): + contents = contents.rstrip() + ret.append(contents) + else: + raise AnsibleParserError() + except AnsibleParserError: + raise AnsibleError("could not locate file in lookup: %s" % term) + + return ret diff --git a/lib/ansible/plugins/lookup/fileglob.py b/lib/ansible/plugins/lookup/fileglob.py new file mode 100644 index 0000000..abf8202 --- /dev/null +++ b/lib/ansible/plugins/lookup/fileglob.py @@ -0,0 +1,84 @@ +# (c) 2012, Michael DeHaan +# (c) 2017 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = """ + name: fileglob + author: Michael DeHaan + version_added: "1.4" + short_description: list files matching a pattern + description: + - Matches all files in a single directory, non-recursively, that match a pattern. + It calls Python's "glob" library. + options: + _terms: + description: path(s) of files to read + required: True + notes: + - Patterns are only supported on files, not directory/paths. + - See R(Ansible task paths,playbook_task_paths) to understand how file lookup occurs with paths. + - Matching is against local system files on the Ansible controller. + To iterate a list of files on a remote node, use the M(ansible.builtin.find) module. + - Returns a string list of paths joined by commas, or an empty list if no files match. For a 'true list' pass C(wantlist=True) to the lookup. +""" + +EXAMPLES = """ +- name: Display paths of all .txt files in dir + ansible.builtin.debug: msg={{ lookup('ansible.builtin.fileglob', '/my/path/*.txt') }} + +- name: Copy each file over that matches the given pattern + ansible.builtin.copy: + src: "{{ item }}" + dest: "/etc/fooapp/" + owner: "root" + mode: 0600 + with_fileglob: + - "/playbooks/files/fooapp/*" +""" + +RETURN = """ + _list: + description: + - list of files + type: list + elements: path +""" + +import os +import glob + +from ansible.plugins.lookup import LookupBase +from ansible.errors import AnsibleFileNotFound +from ansible.module_utils._text import to_bytes, to_text + + +class LookupModule(LookupBase): + + def run(self, terms, variables=None, **kwargs): + + ret = [] + for term in terms: + term_file = os.path.basename(term) + found_paths = [] + if term_file != term: + found_paths.append(self.find_file_in_search_path(variables, 'files', os.path.dirname(term))) + else: + # no dir, just file, so use paths and 'files' paths instead + if 'ansible_search_path' in variables: + paths = variables['ansible_search_path'] + else: + paths = [self.get_basedir(variables)] + for p in paths: + found_paths.append(os.path.join(p, 'files')) + found_paths.append(p) + + for dwimmed_path in found_paths: + if dwimmed_path: + globbed = glob.glob(to_bytes(os.path.join(dwimmed_path, term_file), errors='surrogate_or_strict')) + term_results = [to_text(g, errors='surrogate_or_strict') for g in globbed if os.path.isfile(g)] + if term_results: + ret.extend(term_results) + break + return ret diff --git a/lib/ansible/plugins/lookup/first_found.py b/lib/ansible/plugins/lookup/first_found.py new file mode 100644 index 0000000..5b94b10 --- /dev/null +++ b/lib/ansible/plugins/lookup/first_found.py @@ -0,0 +1,235 @@ +# (c) 2013, seth vidal red hat, inc +# (c) 2017 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = """ + name: first_found + author: Seth Vidal (!UNKNOWN) + version_added: historical + short_description: return first file found from list + description: + - This lookup checks a list of files and paths and returns the full path to the first combination found. + - As all lookups, when fed relative paths it will try use the current task's location first and go up the chain + to the containing locations of role / play / include and so on. + - The list of files has precedence over the paths searched. + For example, A task in a role has a 'file1' in the play's relative path, this will be used, 'file2' in role's relative path will not. + - Either a list of files C(_terms) or a key C(files) with a list of files is required for this plugin to operate. + notes: + - This lookup can be used in 'dual mode', either passing a list of file names or a dictionary that has C(files) and C(paths). + options: + _terms: + description: A list of file names. + files: + description: A list of file names. + type: list + elements: string + default: [] + paths: + description: A list of paths in which to look for the files. + type: list + elements: string + default: [] + skip: + type: boolean + default: False + description: + - When C(True), return an empty list when no files are matched. + - This is useful when used with C(with_first_found), as an empty list return to C(with_) calls + causes the calling task to be skipped. + - When used as a template via C(lookup) or C(query), setting I(skip=True) will *not* cause the task to skip. + Tasks must handle the empty list return from the template. + - When C(False) and C(lookup) or C(query) specifies I(errors='ignore') all errors (including no file found, + but potentially others) return an empty string or an empty list respectively. + - When C(True) and C(lookup) or C(query) specifies I(errors='ignore'), no file found will return an empty + list and other potential errors return an empty string or empty list depending on the template call + (in other words return values of C(lookup) v C(query)). +""" + +EXAMPLES = """ +- name: Set _found_file to the first existing file, raising an error if a file is not found + ansible.builtin.set_fact: + _found_file: "{{ lookup('ansible.builtin.first_found', findme) }}" + vars: + findme: + - /path/to/foo.txt + - bar.txt # will be looked in files/ dir relative to role and/or play + - /path/to/biz.txt + +- name: Set _found_file to the first existing file, or an empty list if no files found + ansible.builtin.set_fact: + _found_file: "{{ lookup('ansible.builtin.first_found', files, paths=['/extra/path'], skip=True) }}" + vars: + files: + - /path/to/foo.txt + - /path/to/bar.txt + +- name: Include tasks only if one of the files exist, otherwise skip the task + ansible.builtin.include_tasks: + file: "{{ item }}" + with_first_found: + - files: + - path/tasks.yaml + - path/other_tasks.yaml + skip: True + +- name: Include tasks only if one of the files exists, otherwise skip + ansible.builtin.include_tasks: '{{ tasks_file }}' + when: tasks_file != "" + vars: + tasks_file: "{{ lookup('ansible.builtin.first_found', files=['tasks.yaml', 'other_tasks.yaml'], errors='ignore') }}" + +- name: | + copy first existing file found to /some/file, + looking in relative directories from where the task is defined and + including any play objects that contain it + ansible.builtin.copy: + src: "{{ lookup('ansible.builtin.first_found', findme) }}" + dest: /some/file + vars: + findme: + - foo + - "{{ inventory_hostname }}" + - bar + +- name: same copy but specific paths + ansible.builtin.copy: + src: "{{ lookup('ansible.builtin.first_found', params) }}" + dest: /some/file + vars: + params: + files: + - foo + - "{{ inventory_hostname }}" + - bar + paths: + - /tmp/production + - /tmp/staging + +- name: INTERFACES | Create Ansible header for /etc/network/interfaces + ansible.builtin.template: + src: "{{ lookup('ansible.builtin.first_found', findme)}}" + dest: "/etc/foo.conf" + vars: + findme: + - "{{ ansible_virtualization_type }}_foo.conf" + - "default_foo.conf" + +- name: read vars from first file found, use 'vars/' relative subdir + ansible.builtin.include_vars: "{{lookup('ansible.builtin.first_found', params)}}" + vars: + params: + files: + - '{{ ansible_distribution }}.yml' + - '{{ ansible_os_family }}.yml' + - default.yml + paths: + - 'vars' +""" + +RETURN = """ + _raw: + description: + - path to file found + type: list + elements: path +""" +import os +import re + +from collections.abc import Mapping, Sequence + +from jinja2.exceptions import UndefinedError + +from ansible.errors import AnsibleLookupError, AnsibleUndefinedVariable +from ansible.module_utils.six import string_types +from ansible.plugins.lookup import LookupBase + + +def _split_on(terms, spliters=','): + termlist = [] + if isinstance(terms, string_types): + termlist = re.split(r'[%s]' % ''.join(map(re.escape, spliters)), terms) + else: + # added since options will already listify + for t in terms: + termlist.extend(_split_on(t, spliters)) + return termlist + + +class LookupModule(LookupBase): + + def _process_terms(self, terms, variables, kwargs): + + total_search = [] + skip = False + + # can use a dict instead of list item to pass inline config + for term in terms: + if isinstance(term, Mapping): + self.set_options(var_options=variables, direct=term) + elif isinstance(term, string_types): + self.set_options(var_options=variables, direct=kwargs) + elif isinstance(term, Sequence): + partial, skip = self._process_terms(term, variables, kwargs) + total_search.extend(partial) + continue + else: + raise AnsibleLookupError("Invalid term supplied, can handle string, mapping or list of strings but got: %s for %s" % (type(term), term)) + + files = self.get_option('files') + paths = self.get_option('paths') + + # NOTE: this is used as 'global' but can be set many times?!?!? + skip = self.get_option('skip') + + # magic extra spliting to create lists + filelist = _split_on(files, ',;') + pathlist = _split_on(paths, ',:;') + + # create search structure + if pathlist: + for path in pathlist: + for fn in filelist: + f = os.path.join(path, fn) + total_search.append(f) + elif filelist: + # NOTE: this seems wrong, should be 'extend' as any option/entry can clobber all + total_search = filelist + else: + total_search.append(term) + + return total_search, skip + + def run(self, terms, variables, **kwargs): + + total_search, skip = self._process_terms(terms, variables, kwargs) + + # NOTE: during refactor noticed that the 'using a dict' as term + # is designed to only work with 'one' otherwise inconsistencies will appear. + # see other notes below. + + # actually search + subdir = getattr(self, '_subdir', 'files') + + path = None + for fn in total_search: + + try: + fn = self._templar.template(fn) + except (AnsibleUndefinedVariable, UndefinedError): + continue + + # get subdir if set by task executor, default to files otherwise + path = self.find_file_in_search_path(variables, subdir, fn, ignore_missing=True) + + # exit if we find one! + if path is not None: + return [path] + + # if we get here, no file was found + if skip: + # NOTE: global skip wont matter, only last 'skip' value in dict term + return [] + raise AnsibleLookupError("No file was found when using first_found.") diff --git a/lib/ansible/plugins/lookup/indexed_items.py b/lib/ansible/plugins/lookup/indexed_items.py new file mode 100644 index 0000000..f63a895 --- /dev/null +++ b/lib/ansible/plugins/lookup/indexed_items.py @@ -0,0 +1,52 @@ +# (c) 2012, Michael DeHaan +# (c) 2017 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = """ + name: indexed_items + author: Michael DeHaan + version_added: "1.3" + short_description: rewrites lists to return 'indexed items' + description: + - use this lookup if you want to loop over an array and also get the numeric index of where you are in the array as you go + - any list given will be transformed with each resulting element having the it's previous position in item.0 and its value in item.1 + options: + _terms: + description: list of items + required: True +""" + +EXAMPLES = """ +- name: indexed loop demo + ansible.builtin.debug: + msg: "at array position {{ item.0 }} there is a value {{ item.1 }}" + with_indexed_items: + - "{{ some_list }}" +""" + +RETURN = """ + _raw: + description: + - list with each item.0 giving you the position and item.1 the value + type: list + elements: list +""" + +from ansible.errors import AnsibleError +from ansible.plugins.lookup import LookupBase + + +class LookupModule(LookupBase): + + def __init__(self, basedir=None, **kwargs): + self.basedir = basedir + + def run(self, terms, variables, **kwargs): + + if not isinstance(terms, list): + raise AnsibleError("with_indexed_items expects a list") + + items = self._flatten(terms) + return list(zip(range(len(items)), items)) diff --git a/lib/ansible/plugins/lookup/ini.py b/lib/ansible/plugins/lookup/ini.py new file mode 100644 index 0000000..eea8634 --- /dev/null +++ b/lib/ansible/plugins/lookup/ini.py @@ -0,0 +1,204 @@ +# (c) 2015, Yannig Perre +# (c) 2017 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = """ + name: ini + author: Yannig Perre (!UNKNOWN) + version_added: "2.0" + short_description: read data from an ini file + description: + - "The ini lookup reads the contents of a file in INI format C(key1=value1). + This plugin retrieves the value on the right side after the equal sign C('=') of a given section C([section])." + - "You can also read a property file which - in this case - does not contain section." + options: + _terms: + description: The key(s) to look up. + required: True + type: + description: Type of the file. 'properties' refers to the Java properties files. + default: 'ini' + choices: ['ini', 'properties'] + file: + description: Name of the file to load. + default: 'ansible.ini' + section: + default: global + description: Section where to lookup the key. + re: + default: False + type: boolean + description: Flag to indicate if the key supplied is a regexp. + encoding: + default: utf-8 + description: Text encoding to use. + default: + description: Return value if the key is not in the ini file. + default: '' + case_sensitive: + description: + Whether key names read from C(file) should be case sensitive. This prevents + duplicate key errors if keys only differ in case. + default: False + version_added: '2.12' + allow_no_value: + description: + - Read an ini file which contains key without value and without '=' symbol. + type: bool + default: False + aliases: ['allow_none'] + version_added: '2.12' +""" + +EXAMPLES = """ +- ansible.builtin.debug: msg="User in integration is {{ lookup('ansible.builtin.ini', 'user', section='integration', file='users.ini') }}" + +- ansible.builtin.debug: msg="User in production is {{ lookup('ansible.builtin.ini', 'user', section='production', file='users.ini') }}" + +- ansible.builtin.debug: msg="user.name is {{ lookup('ansible.builtin.ini', 'user.name', type='properties', file='user.properties') }}" + +- ansible.builtin.debug: + msg: "{{ item }}" + loop: "{{ q('ansible.builtin.ini', '.*', section='section1', file='test.ini', re=True) }}" + +- name: Read an ini file with allow_no_value + ansible.builtin.debug: + msg: "{{ lookup('ansible.builtin.ini', 'user', file='mysql.ini', section='mysqld', allow_no_value=True) }}" +""" + +RETURN = """ +_raw: + description: + - value(s) of the key(s) in the ini file + type: list + elements: str +""" + +import configparser +import os +import re + +from io import StringIO +from collections import defaultdict +from collections.abc import MutableSequence + +from ansible.errors import AnsibleLookupError, AnsibleOptionsError +from ansible.module_utils._text import to_text, to_native +from ansible.plugins.lookup import LookupBase + + +def _parse_params(term, paramvals): + '''Safely split parameter term to preserve spaces''' + + # TODO: deprecate this method + valid_keys = paramvals.keys() + params = defaultdict(lambda: '') + + # TODO: check kv_parser to see if it can handle spaces this same way + keys = [] + thiskey = 'key' # initialize for 'lookup item' + for idp, phrase in enumerate(term.split()): + + # update current key if used + if '=' in phrase: + for k in valid_keys: + if ('%s=' % k) in phrase: + thiskey = k + + # if first term or key does not exist + if idp == 0 or not params[thiskey]: + params[thiskey] = phrase + keys.append(thiskey) + else: + # append to existing key + params[thiskey] += ' ' + phrase + + # return list of values + return [params[x] for x in keys] + + +class LookupModule(LookupBase): + + def get_value(self, key, section, dflt, is_regexp): + # Retrieve all values from a section using a regexp + if is_regexp: + return [v for k, v in self.cp.items(section) if re.match(key, k)] + value = None + # Retrieve a single value + try: + value = self.cp.get(section, key) + except configparser.NoOptionError: + return dflt + return value + + def run(self, terms, variables=None, **kwargs): + + self.set_options(var_options=variables, direct=kwargs) + paramvals = self.get_options() + + self.cp = configparser.ConfigParser(allow_no_value=paramvals.get('allow_no_value', paramvals.get('allow_none'))) + if paramvals['case_sensitive']: + self.cp.optionxform = to_native + + ret = [] + for term in terms: + + key = term + # parameters specified? + if '=' in term or ' ' in term.strip(): + self._deprecate_inline_kv() + params = _parse_params(term, paramvals) + try: + updated_key = False + for param in params: + if '=' in param: + name, value = param.split('=') + if name not in paramvals: + raise AnsibleLookupError('%s is not a valid option.' % name) + paramvals[name] = value + elif key == term: + # only take first, this format never supported multiple keys inline + key = param + updated_key = True + except ValueError as e: + # bad params passed + raise AnsibleLookupError("Could not use '%s' from '%s': %s" % (param, params, to_native(e)), orig_exc=e) + if not updated_key: + raise AnsibleOptionsError("No key to lookup was provided as first term with in string inline options: %s" % term) + # only passed options in inline string + + # TODO: look to use cache to avoid redoing this for every term if they use same file + # Retrieve file path + path = self.find_file_in_search_path(variables, 'files', paramvals['file']) + + # Create StringIO later used to parse ini + config = StringIO() + # Special case for java properties + if paramvals['type'] == "properties": + config.write(u'[java_properties]\n') + paramvals['section'] = 'java_properties' + + # Open file using encoding + contents, show_data = self._loader._get_file_contents(path) + contents = to_text(contents, errors='surrogate_or_strict', encoding=paramvals['encoding']) + config.write(contents) + config.seek(0, os.SEEK_SET) + + try: + self.cp.readfp(config) + except configparser.DuplicateOptionError as doe: + raise AnsibleLookupError("Duplicate option in '{file}': {error}".format(file=paramvals['file'], error=to_native(doe))) + + try: + var = self.get_value(key, paramvals['section'], paramvals['default'], paramvals['re']) + except configparser.NoSectionError: + raise AnsibleLookupError("No section '{section}' in {file}".format(section=paramvals['section'], file=paramvals['file'])) + if var is not None: + if isinstance(var, MutableSequence): + for v in var: + ret.append(v) + else: + ret.append(var) + return ret diff --git a/lib/ansible/plugins/lookup/inventory_hostnames.py b/lib/ansible/plugins/lookup/inventory_hostnames.py new file mode 100644 index 0000000..4fa1d68 --- /dev/null +++ b/lib/ansible/plugins/lookup/inventory_hostnames.py @@ -0,0 +1,53 @@ +# (c) 2012, Michael DeHaan +# (c) 2013, Steven Dossett +# (c) 2017 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = """ + name: inventory_hostnames + author: + - Michael DeHaan + - Steven Dossett (!UNKNOWN) + version_added: "1.3" + short_description: list of inventory hosts matching a host pattern + description: + - "This lookup understands 'host patterns' as used by the C(hosts:) keyword in plays + and can return a list of matching hosts from inventory" + notes: + - this is only worth for 'hostname patterns' it is easier to loop over the group/group_names variables otherwise. +""" + +EXAMPLES = """ +- name: show all the hosts matching the pattern, i.e. all but the group www + ansible.builtin.debug: + msg: "{{ item }}" + with_inventory_hostnames: + - all:!www +""" + +RETURN = """ + _hostnames: + description: list of hostnames that matched the host pattern in inventory + type: list +""" + +from ansible.errors import AnsibleError +from ansible.inventory.manager import InventoryManager +from ansible.plugins.lookup import LookupBase + + +class LookupModule(LookupBase): + def run(self, terms, variables=None, **kwargs): + manager = InventoryManager(self._loader, parse=False) + for group, hosts in variables['groups'].items(): + manager.add_group(group) + for host in hosts: + manager.add_host(host, group=group) + + try: + return [h.name for h in manager.get_hosts(pattern=terms)] + except AnsibleError: + return [] diff --git a/lib/ansible/plugins/lookup/items.py b/lib/ansible/plugins/lookup/items.py new file mode 100644 index 0000000..162c1e7 --- /dev/null +++ b/lib/ansible/plugins/lookup/items.py @@ -0,0 +1,73 @@ +# (c) 2012, Michael DeHaan +# (c) 2017 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = """ + name: items + author: Michael DeHaan + version_added: historical + short_description: list of items + description: + - this lookup returns a list of items given to it, if any of the top level items is also a list it will flatten it, but it will not recurse + notes: + - this is the standard lookup used for loops in most examples + - check out the 'flattened' lookup for recursive flattening + - if you do not want flattening nor any other transformation look at the 'list' lookup. + options: + _terms: + description: list of items + required: True +""" + +EXAMPLES = """ +- name: "loop through list" + ansible.builtin.debug: + msg: "An item: {{ item }}" + with_items: + - 1 + - 2 + - 3 + +- name: add several users + ansible.builtin.user: + name: "{{ item }}" + groups: "wheel" + state: present + with_items: + - testuser1 + - testuser2 + +- name: "loop through list from a variable" + ansible.builtin.debug: + msg: "An item: {{ item }}" + with_items: "{{ somelist }}" + +- name: more complex items to add several users + ansible.builtin.user: + name: "{{ item.name }}" + uid: "{{ item.uid }}" + groups: "{{ item.groups }}" + state: present + with_items: + - { name: testuser1, uid: 1002, groups: "wheel, staff" } + - { name: testuser2, uid: 1003, groups: staff } + +""" + +RETURN = """ + _raw: + description: + - once flattened list + type: list +""" + +from ansible.plugins.lookup import LookupBase + + +class LookupModule(LookupBase): + + def run(self, terms, **kwargs): + + return self._flatten(terms) diff --git a/lib/ansible/plugins/lookup/lines.py b/lib/ansible/plugins/lookup/lines.py new file mode 100644 index 0000000..7676d01 --- /dev/null +++ b/lib/ansible/plugins/lookup/lines.py @@ -0,0 +1,62 @@ +# (c) 2012, Daniel Hokka Zakrisson +# (c) 2017 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = """ + name: lines + author: Daniel Hokka Zakrisson (!UNKNOWN) + version_added: "0.9" + short_description: read lines from command + description: + - Run one or more commands and split the output into lines, returning them as a list + options: + _terms: + description: command(s) to run + required: True + notes: + - Like all lookups, this runs on the Ansible controller and is unaffected by other keywords such as 'become'. + If you need to use different permissions, you must change the command or run Ansible as another user. + - Alternatively, you can use a shell/command task that runs against localhost and registers the result. +""" + +EXAMPLES = """ +- name: We could read the file directly, but this shows output from command + ansible.builtin.debug: msg="{{ item }} is an output line from running cat on /etc/motd" + with_lines: cat /etc/motd + +- name: More useful example of looping over a command result + ansible.builtin.shell: "/usr/bin/frobnicate {{ item }}" + with_lines: + - "/usr/bin/frobnications_per_host --param {{ inventory_hostname }}" +""" + +RETURN = """ + _list: + description: + - lines of stdout from command + type: list + elements: str +""" + +import subprocess +from ansible.errors import AnsibleError +from ansible.plugins.lookup import LookupBase +from ansible.module_utils._text import to_text + + +class LookupModule(LookupBase): + + def run(self, terms, variables, **kwargs): + + ret = [] + for term in terms: + p = subprocess.Popen(term, cwd=self._loader.get_basedir(), shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE) + (stdout, stderr) = p.communicate() + if p.returncode == 0: + ret.extend([to_text(l) for l in stdout.splitlines()]) + else: + raise AnsibleError("lookup_plugin.lines(%s) returned %d" % (term, p.returncode)) + return ret diff --git a/lib/ansible/plugins/lookup/list.py b/lib/ansible/plugins/lookup/list.py new file mode 100644 index 0000000..6c553ae --- /dev/null +++ b/lib/ansible/plugins/lookup/list.py @@ -0,0 +1,45 @@ +# (c) 2012-17 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +# Make coding more python3-ish +from __future__ import (absolute_import, division, print_function) + +__metaclass__ = type + +DOCUMENTATION = """ + name: list + author: Ansible Core Team + version_added: "2.0" + short_description: simply returns what it is given. + description: + - this is mostly a noop, to be used as a with_list loop when you dont want the content transformed in any way. +""" + +EXAMPLES = """ +- name: unlike with_items you will get 3 items from this loop, the 2nd one being a list + ansible.builtin.debug: var=item + with_list: + - 1 + - [2,3] + - 4 +""" + +RETURN = """ + _list: + description: basically the same as you fed in + type: list + elements: raw +""" + +from collections.abc import Sequence + +from ansible.plugins.lookup import LookupBase +from ansible.errors import AnsibleError + + +class LookupModule(LookupBase): + + def run(self, terms, **kwargs): + if not isinstance(terms, Sequence): + raise AnsibleError("with_list expects a list") + return terms diff --git a/lib/ansible/plugins/lookup/nested.py b/lib/ansible/plugins/lookup/nested.py new file mode 100644 index 0000000..e768dba --- /dev/null +++ b/lib/ansible/plugins/lookup/nested.py @@ -0,0 +1,85 @@ +# (c) 2012, Michael DeHaan +# (c) 2017 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = """ + name: nested + version_added: "1.1" + short_description: composes a list with nested elements of other lists + description: + - Takes the input lists and returns a list with elements that are lists composed of the elements of the input lists + options: + _raw: + description: + - a set of lists + required: True +""" + +EXAMPLES = """ +- name: give users access to multiple databases + community.mysql.mysql_user: + name: "{{ item[0] }}" + priv: "{{ item[1] }}.*:ALL" + append_privs: yes + password: "foo" + with_nested: + - [ 'alice', 'bob' ] + - [ 'clientdb', 'employeedb', 'providerdb' ] +# As with the case of 'with_items' above, you can use previously defined variables.: + +- name: here, 'users' contains the above list of employees + community.mysql.mysql_user: + name: "{{ item[0] }}" + priv: "{{ item[1] }}.*:ALL" + append_privs: yes + password: "foo" + with_nested: + - "{{ users }}" + - [ 'clientdb', 'employeedb', 'providerdb' ] +""" + +RETURN = """ + _list: + description: + - A list composed of lists paring the elements of the input lists + type: list +""" + +from jinja2.exceptions import UndefinedError + +from ansible.errors import AnsibleError, AnsibleUndefinedVariable +from ansible.plugins.lookup import LookupBase +from ansible.utils.listify import listify_lookup_plugin_terms + + +class LookupModule(LookupBase): + + def _lookup_variables(self, terms, variables): + results = [] + for x in terms: + try: + intermediate = listify_lookup_plugin_terms(x, templar=self._templar, fail_on_undefined=True) + except UndefinedError as e: + raise AnsibleUndefinedVariable("One of the nested variables was undefined. The error was: %s" % e) + results.append(intermediate) + return results + + def run(self, terms, variables=None, **kwargs): + + terms = self._lookup_variables(terms, variables) + + my_list = terms[:] + my_list.reverse() + result = [] + if len(my_list) == 0: + raise AnsibleError("with_nested requires at least one element in the nested list") + result = my_list.pop() + while len(my_list) > 0: + result2 = self._combine(result, my_list.pop()) + result = result2 + new_result = [] + for x in result: + new_result.append(self._flatten(x)) + return new_result diff --git a/lib/ansible/plugins/lookup/password.py b/lib/ansible/plugins/lookup/password.py new file mode 100644 index 0000000..06ea8b3 --- /dev/null +++ b/lib/ansible/plugins/lookup/password.py @@ -0,0 +1,389 @@ +# (c) 2012, Daniel Hokka Zakrisson +# (c) 2013, Javier Candeira +# (c) 2013, Maykel Moya +# (c) 2017 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = """ + name: password + version_added: "1.1" + author: + - Daniel Hokka Zakrisson (!UNKNOWN) + - Javier Candeira (!UNKNOWN) + - Maykel Moya (!UNKNOWN) + short_description: retrieve or generate a random password, stored in a file + description: + - Generates a random plaintext password and stores it in a file at a given filepath. + - If the file exists previously, it will retrieve its contents, behaving just like with_file. + - 'Usage of variables like C("{{ inventory_hostname }}") in the filepath can be used to set up random passwords per host, + which simplifies password management in C("host_vars") variables.' + - A special case is using /dev/null as a path. The password lookup will generate a new random password each time, + but will not write it to /dev/null. This can be used when you need a password without storing it on the controller. + options: + _terms: + description: + - path to the file that stores/will store the passwords + required: True + encrypt: + description: + - Which hash scheme to encrypt the returning password, should be one hash scheme from C(passlib.hash; md5_crypt, bcrypt, sha256_crypt, sha512_crypt). + - If not provided, the password will be returned in plain text. + - Note that the password is always stored as plain text, only the returning password is encrypted. + - Encrypt also forces saving the salt value for idempotence. + - Note that before 2.6 this option was incorrectly labeled as a boolean for a long time. + ident: + description: + - Specify version of Bcrypt algorithm to be used while using C(encrypt) as C(bcrypt). + - The parameter is only available for C(bcrypt) - U(https://passlib.readthedocs.io/en/stable/lib/passlib.hash.bcrypt.html#passlib.hash.bcrypt). + - Other hash types will simply ignore this parameter. + - 'Valid values for this parameter are: C(2), C(2a), C(2y), C(2b).' + type: string + version_added: "2.12" + chars: + version_added: "1.4" + description: + - A list of names that compose a custom character set in the generated passwords. + - 'By default generated passwords contain a random mix of upper and lowercase ASCII letters, the numbers 0-9, and punctuation (". , : - _").' + - "They can be either parts of Python's string module attributes or represented literally ( :, -)." + - "Though string modules can vary by Python version, valid values for both major releases include: + 'ascii_lowercase', 'ascii_uppercase', 'digits', 'hexdigits', 'octdigits', 'printable', 'punctuation' and 'whitespace'." + - Be aware that Python's 'hexdigits' includes lower and upper case versions of a-f, so it is not a good choice as it doubles + the chances of those values for systems that won't distinguish case, distorting the expected entropy. + - "when using a comma separated string, to enter comma use two commas ',,' somewhere - preferably at the end. + Quotes and double quotes are not supported." + type: list + elements: str + default: ['ascii_letters', 'digits', ".,:-_"] + length: + description: The length of the generated password. + default: 20 + type: integer + seed: + version_added: "2.12" + description: + - A seed to initialize the random number generator. + - Identical seeds will yield identical passwords. + - Use this for random-but-idempotent password generation. + type: str + notes: + - A great alternative to the password lookup plugin, + if you don't need to generate random passwords on a per-host basis, + would be to use Vault in playbooks. + Read the documentation there and consider using it first, + it will be more desirable for most applications. + - If the file already exists, no data will be written to it. + If the file has contents, those contents will be read in as the password. + Empty files cause the password to return as an empty string. + - 'As all lookups, this runs on the Ansible host as the user running the playbook, and "become" does not apply, + the target file must be readable by the playbook user, or, if it does not exist, + the playbook user must have sufficient privileges to create it. + (So, for example, attempts to write into areas such as /etc will fail unless the entire playbook is being run as root).' +""" + +EXAMPLES = """ +- name: create a mysql user with a random password + community.mysql.mysql_user: + name: "{{ client }}" + password: "{{ lookup('ansible.builtin.password', 'credentials/' + client + '/' + tier + '/' + role + '/mysqlpassword length=15') }}" + priv: "{{ client }}_{{ tier }}_{{ role }}.*:ALL" + +- name: create a mysql user with a random password using only ascii letters + community.mysql.mysql_user: + name: "{{ client }}" + password: "{{ lookup('ansible.builtin.password', '/tmp/passwordfile chars=ascii_letters') }}" + priv: '{{ client }}_{{ tier }}_{{ role }}.*:ALL' + +- name: create a mysql user with an 8 character random password using only digits + community.mysql.mysql_user: + name: "{{ client }}" + password: "{{ lookup('ansible.builtin.password', '/tmp/passwordfile length=8 chars=digits') }}" + priv: "{{ client }}_{{ tier }}_{{ role }}.*:ALL" + +- name: create a mysql user with a random password using many different char sets + community.mysql.mysql_user: + name: "{{ client }}" + password: "{{ lookup('ansible.builtin.password', '/tmp/passwordfile chars=ascii_letters,digits,punctuation') }}" + priv: "{{ client }}_{{ tier }}_{{ role }}.*:ALL" + +- name: create lowercase 8 character name for Kubernetes pod name + ansible.builtin.set_fact: + random_pod_name: "web-{{ lookup('ansible.builtin.password', '/dev/null chars=ascii_lowercase,digits length=8') }}" + +- name: create random but idempotent password + ansible.builtin.set_fact: + password: "{{ lookup('ansible.builtin.password', '/dev/null', seed=inventory_hostname) }}" +""" + +RETURN = """ +_raw: + description: + - a password + type: list + elements: str +""" + +import os +import string +import time +import hashlib + +from ansible.errors import AnsibleError, AnsibleAssertionError +from ansible.module_utils._text import to_bytes, to_native, to_text +from ansible.module_utils.six import string_types +from ansible.parsing.splitter import parse_kv +from ansible.plugins.lookup import LookupBase +from ansible.utils.encrypt import BaseHash, do_encrypt, random_password, random_salt +from ansible.utils.path import makedirs_safe + + +VALID_PARAMS = frozenset(('length', 'encrypt', 'chars', 'ident', 'seed')) + + +def _read_password_file(b_path): + """Read the contents of a password file and return it + :arg b_path: A byte string containing the path to the password file + :returns: a text string containing the contents of the password file or + None if no password file was present. + """ + content = None + + if os.path.exists(b_path): + with open(b_path, 'rb') as f: + b_content = f.read().rstrip() + content = to_text(b_content, errors='surrogate_or_strict') + + return content + + +def _gen_candidate_chars(characters): + '''Generate a string containing all valid chars as defined by ``characters`` + + :arg characters: A list of character specs. The character specs are + shorthand names for sets of characters like 'digits', 'ascii_letters', + or 'punctuation' or a string to be included verbatim. + + The values of each char spec can be: + + * a name of an attribute in the 'strings' module ('digits' for example). + The value of the attribute will be added to the candidate chars. + * a string of characters. If the string isn't an attribute in 'string' + module, the string will be directly added to the candidate chars. + + For example:: + + characters=['digits', '?|']`` + + will match ``string.digits`` and add all ascii digits. ``'?|'`` will add + the question mark and pipe characters directly. Return will be the string:: + + u'0123456789?|' + ''' + chars = [] + for chars_spec in characters: + # getattr from string expands things like "ascii_letters" and "digits" + # into a set of characters. + chars.append(to_text(getattr(string, to_native(chars_spec), chars_spec), errors='strict')) + chars = u''.join(chars).replace(u'"', u'').replace(u"'", u'') + return chars + + +def _parse_content(content): + '''parse our password data format into password and salt + + :arg content: The data read from the file + :returns: password and salt + ''' + password = content + salt = None + + salt_slug = u' salt=' + try: + sep = content.rindex(salt_slug) + except ValueError: + # No salt + pass + else: + salt = password[sep + len(salt_slug):] + password = content[:sep] + + return password, salt + + +def _format_content(password, salt, encrypt=None, ident=None): + """Format the password and salt for saving + :arg password: the plaintext password to save + :arg salt: the salt to use when encrypting a password + :arg encrypt: Which method the user requests that this password is encrypted. + Note that the password is saved in clear. Encrypt just tells us if we + must save the salt value for idempotence. Defaults to None. + :arg ident: Which version of BCrypt algorithm to be used. + Valid only if value of encrypt is bcrypt. + Defaults to None. + :returns: a text string containing the formatted information + + .. warning:: Passwords are saved in clear. This is because the playbooks + expect to get cleartext passwords from this lookup. + """ + if not encrypt and not salt: + return password + + # At this point, the calling code should have assured us that there is a salt value. + if not salt: + raise AnsibleAssertionError('_format_content was called with encryption requested but no salt value') + + if ident: + return u'%s salt=%s ident=%s' % (password, salt, ident) + return u'%s salt=%s' % (password, salt) + + +def _write_password_file(b_path, content): + b_pathdir = os.path.dirname(b_path) + makedirs_safe(b_pathdir, mode=0o700) + + with open(b_path, 'wb') as f: + os.chmod(b_path, 0o600) + b_content = to_bytes(content, errors='surrogate_or_strict') + b'\n' + f.write(b_content) + + +def _get_lock(b_path): + """Get the lock for writing password file.""" + first_process = False + b_pathdir = os.path.dirname(b_path) + lockfile_name = to_bytes("%s.ansible_lockfile" % hashlib.sha1(b_path).hexdigest()) + lockfile = os.path.join(b_pathdir, lockfile_name) + if not os.path.exists(lockfile) and b_path != to_bytes('/dev/null'): + try: + makedirs_safe(b_pathdir, mode=0o700) + fd = os.open(lockfile, os.O_CREAT | os.O_EXCL) + os.close(fd) + first_process = True + except OSError as e: + if e.strerror != 'File exists': + raise + + counter = 0 + # if the lock is got by other process, wait until it's released + while os.path.exists(lockfile) and not first_process: + time.sleep(2 ** counter) + if counter >= 2: + raise AnsibleError("Password lookup cannot get the lock in 7 seconds, abort..." + "This may caused by un-removed lockfile" + "you can manually remove it from controller machine at %s and try again" % lockfile) + counter += 1 + return first_process, lockfile + + +def _release_lock(lockfile): + """Release the lock so other processes can read the password file.""" + if os.path.exists(lockfile): + os.remove(lockfile) + + +class LookupModule(LookupBase): + + def _parse_parameters(self, term): + """Hacky parsing of params + + See https://github.com/ansible/ansible-modules-core/issues/1968#issuecomment-136842156 + and the first_found lookup For how we want to fix this later + """ + first_split = term.split(' ', 1) + if len(first_split) <= 1: + # Only a single argument given, therefore it's a path + relpath = term + params = dict() + else: + relpath = first_split[0] + params = parse_kv(first_split[1]) + if '_raw_params' in params: + # Spaces in the path? + relpath = u' '.join((relpath, params['_raw_params'])) + del params['_raw_params'] + + # Check that we parsed the params correctly + if not term.startswith(relpath): + # Likely, the user had a non parameter following a parameter. + # Reject this as a user typo + raise AnsibleError('Unrecognized value after key=value parameters given to password lookup') + # No _raw_params means we already found the complete path when + # we split it initially + + # Check for invalid parameters. Probably a user typo + invalid_params = frozenset(params.keys()).difference(VALID_PARAMS) + if invalid_params: + raise AnsibleError('Unrecognized parameter(s) given to password lookup: %s' % ', '.join(invalid_params)) + + # Set defaults + params['length'] = int(params.get('length', self.get_option('length'))) + params['encrypt'] = params.get('encrypt', self.get_option('encrypt')) + params['ident'] = params.get('ident', self.get_option('ident')) + params['seed'] = params.get('seed', self.get_option('seed')) + + params['chars'] = params.get('chars', self.get_option('chars')) + if params['chars'] and isinstance(params['chars'], string_types): + tmp_chars = [] + if u',,' in params['chars']: + tmp_chars.append(u',') + tmp_chars.extend(c for c in params['chars'].replace(u',,', u',').split(u',') if c) + params['chars'] = tmp_chars + + return relpath, params + + def run(self, terms, variables, **kwargs): + ret = [] + + self.set_options(var_options=variables, direct=kwargs) + + for term in terms: + relpath, params = self._parse_parameters(term) + path = self._loader.path_dwim(relpath) + b_path = to_bytes(path, errors='surrogate_or_strict') + chars = _gen_candidate_chars(params['chars']) + + changed = None + # make sure only one process finishes all the job first + first_process, lockfile = _get_lock(b_path) + + content = _read_password_file(b_path) + + if content is None or b_path == to_bytes('/dev/null'): + plaintext_password = random_password(params['length'], chars, params['seed']) + salt = None + changed = True + else: + plaintext_password, salt = _parse_content(content) + + encrypt = params['encrypt'] + if encrypt and not salt: + changed = True + try: + salt = random_salt(BaseHash.algorithms[encrypt].salt_size) + except KeyError: + salt = random_salt() + + ident = params['ident'] + if encrypt and not ident: + changed = True + try: + ident = BaseHash.algorithms[encrypt].implicit_ident + except KeyError: + ident = None + + if changed and b_path != to_bytes('/dev/null'): + content = _format_content(plaintext_password, salt, encrypt=encrypt, ident=ident) + _write_password_file(b_path, content) + + if first_process: + # let other processes continue + _release_lock(lockfile) + + if encrypt: + password = do_encrypt(plaintext_password, encrypt, salt=salt, ident=ident) + ret.append(password) + else: + ret.append(plaintext_password) + + return ret diff --git a/lib/ansible/plugins/lookup/pipe.py b/lib/ansible/plugins/lookup/pipe.py new file mode 100644 index 0000000..54df3fc --- /dev/null +++ b/lib/ansible/plugins/lookup/pipe.py @@ -0,0 +1,76 @@ +# (c) 2012, Daniel Hokka Zakrisson +# (c) 2017 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = r""" + name: pipe + author: Daniel Hokka Zakrisson (!UNKNOWN) + version_added: "0.9" + short_description: read output from a command + description: + - Run a command and return the output. + options: + _terms: + description: command(s) to run. + required: True + notes: + - Like all lookups this runs on the Ansible controller and is unaffected by other keywords, such as become, + so if you need to different permissions you must change the command or run Ansible as another user. + - Alternatively you can use a shell/command task that runs against localhost and registers the result. + - Pipe lookup internally invokes Popen with shell=True (this is required and intentional). + This type of invocation is considered a security issue if appropriate care is not taken to sanitize any user provided or variable input. + It is strongly recommended to pass user input or variable input via quote filter before using with pipe lookup. + See example section for this. + Read more about this L(Bandit B602 docs,https://bandit.readthedocs.io/en/latest/plugins/b602_subprocess_popen_with_shell_equals_true.html) +""" + +EXAMPLES = r""" +- name: raw result of running date command + ansible.builtin.debug: + msg: "{{ lookup('ansible.builtin.pipe', 'date') }}" + +- name: Always use quote filter to make sure your variables are safe to use with shell + ansible.builtin.debug: + msg: "{{ lookup('ansible.builtin.pipe', 'getent passwd ' + myuser | quote ) }}" +""" + +RETURN = r""" + _string: + description: + - stdout from command + type: list + elements: str +""" + +import subprocess + +from ansible.errors import AnsibleError +from ansible.plugins.lookup import LookupBase + + +class LookupModule(LookupBase): + + def run(self, terms, variables, **kwargs): + + ret = [] + for term in terms: + ''' + https://docs.python.org/3/library/subprocess.html#popen-constructor + + The shell argument (which defaults to False) specifies whether to use the + shell as the program to execute. If shell is True, it is recommended to pass + args as a string rather than as a sequence + + https://github.com/ansible/ansible/issues/6550 + ''' + term = str(term) + + p = subprocess.Popen(term, cwd=self._loader.get_basedir(), shell=True, stdin=subprocess.PIPE, stdout=subprocess.PIPE) + (stdout, stderr) = p.communicate() + if p.returncode == 0: + ret.append(stdout.decode("utf-8").rstrip()) + else: + raise AnsibleError("lookup_plugin.pipe(%s) returned %d" % (term, p.returncode)) + return ret diff --git a/lib/ansible/plugins/lookup/random_choice.py b/lib/ansible/plugins/lookup/random_choice.py new file mode 100644 index 0000000..9f8a6ae --- /dev/null +++ b/lib/ansible/plugins/lookup/random_choice.py @@ -0,0 +1,53 @@ +# (c) 2013, Michael DeHaan +# (c) 2017 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = """ + name: random_choice + author: Michael DeHaan + version_added: "1.1" + short_description: return random element from list + description: + - The 'random_choice' feature can be used to pick something at random. While it's not a load balancer (there are modules for those), + it can somewhat be used as a poor man's load balancer in a MacGyver like situation. + - At a more basic level, they can be used to add chaos and excitement to otherwise predictable automation environments. +""" + +EXAMPLES = """ +- name: Magic 8 ball for MUDs + ansible.builtin.debug: + msg: "{{ item }}" + with_random_choice: + - "go through the door" + - "drink from the goblet" + - "press the red button" + - "do nothing" +""" + +RETURN = """ + _raw: + description: + - random item + type: raw +""" +import random + +from ansible.errors import AnsibleError +from ansible.module_utils._text import to_native +from ansible.plugins.lookup import LookupBase + + +class LookupModule(LookupBase): + + def run(self, terms, inject=None, **kwargs): + + ret = terms + if terms: + try: + ret = [random.choice(terms)] + except Exception as e: + raise AnsibleError("Unable to choose random term: %s" % to_native(e)) + + return ret diff --git a/lib/ansible/plugins/lookup/sequence.py b/lib/ansible/plugins/lookup/sequence.py new file mode 100644 index 0000000..8a000c5 --- /dev/null +++ b/lib/ansible/plugins/lookup/sequence.py @@ -0,0 +1,268 @@ +# (c) 2013, Jayson Vantuyl +# (c) 2012-17 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = """ + name: sequence + author: Jayson Vantuyl (!UNKNOWN) + version_added: "1.0" + short_description: generate a list based on a number sequence + description: + - generates a sequence of items. You can specify a start value, an end value, an optional "stride" value that specifies the number of steps + to increment the sequence, and an optional printf-style format string. + - 'Arguments can be specified as key=value pair strings or as a shortcut form of the arguments string is also accepted: [start-]end[/stride][:format].' + - 'Numerical values can be specified in decimal, hexadecimal (0x3f8) or octal (0600).' + - Starting at version 1.9.2, negative strides are allowed. + - Generated items are strings. Use Jinja2 filters to convert items to preferred type, e.g. C({{ 1 + item|int }}). + - See also Jinja2 C(range) filter as an alternative. + options: + start: + description: number at which to start the sequence + default: 0 + type: integer + end: + description: number at which to end the sequence, dont use this with count + type: integer + default: 0 + count: + description: number of elements in the sequence, this is not to be used with end + type: integer + default: 0 + stride: + description: increments between sequence numbers, the default is 1 unless the end is less than the start, then it is -1. + type: integer + format: + description: return a string with the generated number formatted in +""" + +EXAMPLES = """ +- name: create some test users + ansible.builtin.user: + name: "{{ item }}" + state: present + groups: "evens" + with_sequence: start=0 end=32 format=testuser%02x + +- name: create a series of directories with even numbers for some reason + ansible.builtin.file: + dest: "/var/stuff/{{ item }}" + state: directory + with_sequence: start=4 end=16 stride=2 + +- name: a simpler way to use the sequence plugin create 4 groups + ansible.builtin.group: + name: "group{{ item }}" + state: present + with_sequence: count=4 + +- name: the final countdown + ansible.builtin.debug: + msg: "{{item}} seconds to detonation" + with_sequence: start=10 end=0 stride=-1 + +- name: Use of variable + ansible.builtin.debug: + msg: "{{ item }}" + with_sequence: start=1 end="{{ end_at }}" + vars: + - end_at: 10 +""" + +RETURN = """ + _list: + description: + - A list containing generated sequence of items + type: list + elements: str +""" + +from re import compile as re_compile, IGNORECASE + +from ansible.errors import AnsibleError +from ansible.parsing.splitter import parse_kv +from ansible.plugins.lookup import LookupBase + + +# shortcut format +NUM = "(0?x?[0-9a-f]+)" +SHORTCUT = re_compile( + "^(" + # Group 0 + NUM + # Group 1: Start + "-)?" + + NUM + # Group 2: End + "(/" + # Group 3 + NUM + # Group 4: Stride + ")?" + + "(:(.+))?$", # Group 5, Group 6: Format String + IGNORECASE +) + + +class LookupModule(LookupBase): + """ + sequence lookup module + + Used to generate some sequence of items. Takes arguments in two forms. + + The simple / shortcut form is: + + [start-]end[/stride][:format] + + As indicated by the brackets: start, stride, and format string are all + optional. The format string is in the style of printf. This can be used + to pad with zeros, format in hexadecimal, etc. All of the numerical values + can be specified in octal (i.e. 0664) or hexadecimal (i.e. 0x3f8). + Negative numbers are not supported. + + Some examples: + + 5 -> ["1","2","3","4","5"] + 5-8 -> ["5", "6", "7", "8"] + 2-10/2 -> ["2", "4", "6", "8", "10"] + 4:host%02d -> ["host01","host02","host03","host04"] + + The standard Ansible key-value form is accepted as well. For example: + + start=5 end=11 stride=2 format=0x%02x -> ["0x05","0x07","0x09","0x0a"] + + This format takes an alternate form of "end" called "count", which counts + some number from the starting value. For example: + + count=5 -> ["1", "2", "3", "4", "5"] + start=0x0f00 count=4 format=%04x -> ["0f00", "0f01", "0f02", "0f03"] + start=0 count=5 stride=2 -> ["0", "2", "4", "6", "8"] + start=1 count=5 stride=2 -> ["1", "3", "5", "7", "9"] + + The count option is mostly useful for avoiding off-by-one errors and errors + calculating the number of entries in a sequence when a stride is specified. + """ + + def reset(self): + """set sensible defaults""" + self.start = 1 + self.count = None + self.end = None + self.stride = 1 + self.format = "%d" + + def parse_kv_args(self, args): + """parse key-value style arguments""" + for arg in ["start", "end", "count", "stride"]: + try: + arg_raw = args.pop(arg, None) + if arg_raw is None: + continue + arg_cooked = int(arg_raw, 0) + setattr(self, arg, arg_cooked) + except ValueError: + raise AnsibleError( + "can't parse %s=%s as integer" + % (arg, arg_raw) + ) + if 'format' in args: + self.format = args.pop("format") + if args: + raise AnsibleError( + "unrecognized arguments to with_sequence: %s" + % list(args.keys()) + ) + + def parse_simple_args(self, term): + """parse the shortcut forms, return True/False""" + match = SHORTCUT.match(term) + if not match: + return False + + _, start, end, _, stride, _, format = match.groups() + + if start is not None: + try: + start = int(start, 0) + except ValueError: + raise AnsibleError("can't parse start=%s as integer" % start) + if end is not None: + try: + end = int(end, 0) + except ValueError: + raise AnsibleError("can't parse end=%s as integer" % end) + if stride is not None: + try: + stride = int(stride, 0) + except ValueError: + raise AnsibleError("can't parse stride=%s as integer" % stride) + + if start is not None: + self.start = start + if end is not None: + self.end = end + if stride is not None: + self.stride = stride + if format is not None: + self.format = format + + return True + + def sanity_check(self): + if self.count is None and self.end is None: + raise AnsibleError("must specify count or end in with_sequence") + elif self.count is not None and self.end is not None: + raise AnsibleError("can't specify both count and end in with_sequence") + elif self.count is not None: + # convert count to end + if self.count != 0: + self.end = self.start + self.count * self.stride - 1 + else: + self.start = 0 + self.end = 0 + self.stride = 0 + del self.count + if self.stride > 0 and self.end < self.start: + raise AnsibleError("to count backwards make stride negative") + if self.stride < 0 and self.end > self.start: + raise AnsibleError("to count forward don't make stride negative") + if self.format.count('%') != 1: + raise AnsibleError("bad formatting string: %s" % self.format) + + def generate_sequence(self): + if self.stride >= 0: + adjust = 1 + else: + adjust = -1 + numbers = range(self.start, self.end + adjust, self.stride) + + for i in numbers: + try: + formatted = self.format % i + yield formatted + except (ValueError, TypeError): + raise AnsibleError( + "problem formatting %r with %r" % (i, self.format) + ) + + def run(self, terms, variables, **kwargs): + results = [] + + for term in terms: + try: + self.reset() # clear out things for this iteration + try: + if not self.parse_simple_args(term): + self.parse_kv_args(parse_kv(term)) + except AnsibleError: + raise + except Exception as e: + raise AnsibleError("unknown error parsing with_sequence arguments: %r. Error was: %s" % (term, e)) + + self.sanity_check() + if self.stride != 0: + results.extend(self.generate_sequence()) + except AnsibleError: + raise + except Exception as e: + raise AnsibleError( + "unknown error generating sequence: %s" % e + ) + + return results diff --git a/lib/ansible/plugins/lookup/subelements.py b/lib/ansible/plugins/lookup/subelements.py new file mode 100644 index 0000000..9b1af8b --- /dev/null +++ b/lib/ansible/plugins/lookup/subelements.py @@ -0,0 +1,169 @@ +# (c) 2013, Serge van Ginderachter +# (c) 2012-17 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = """ + name: subelements + author: Serge van Ginderachter (!UNKNOWN) + version_added: "1.4" + short_description: traverse nested key from a list of dictionaries + description: + - Subelements walks a list of hashes (aka dictionaries) and then traverses a list with a given (nested sub-)key inside of those records. + options: + _terms: + description: tuple of list of dictionaries and dictionary key to extract + required: True + skip_missing: + default: False + description: + - Lookup accepts this flag from a dictionary as optional. See Example section for more information. + - If set to C(True), the lookup plugin will skip the lists items that do not contain the given subkey. + - If set to C(False), the plugin will yield an error and complain about the missing subkey. +""" + +EXAMPLES = """ +- name: show var structure as it is needed for example to make sense + hosts: all + vars: + users: + - name: alice + authorized: + - /tmp/alice/onekey.pub + - /tmp/alice/twokey.pub + mysql: + password: mysql-password + hosts: + - "%" + - "127.0.0.1" + - "::1" + - "localhost" + privs: + - "*.*:SELECT" + - "DB1.*:ALL" + groups: + - wheel + - name: bob + authorized: + - /tmp/bob/id_rsa.pub + mysql: + password: other-mysql-password + hosts: + - "db1" + privs: + - "*.*:SELECT" + - "DB2.*:ALL" + tasks: + - name: Set authorized ssh key, extracting just that data from 'users' + ansible.posix.authorized_key: + user: "{{ item.0.name }}" + key: "{{ lookup('file', item.1) }}" + with_subelements: + - "{{ users }}" + - authorized + + - name: Setup MySQL users, given the mysql hosts and privs subkey lists + community.mysql.mysql_user: + name: "{{ item.0.name }}" + password: "{{ item.0.mysql.password }}" + host: "{{ item.1 }}" + priv: "{{ item.0.mysql.privs | join('/') }}" + with_subelements: + - "{{ users }}" + - mysql.hosts + + - name: list groups for users that have them, don't error if groups key is missing + ansible.builtin.debug: var=item + loop: "{{ q('ansible.builtin.subelements', users, 'groups', {'skip_missing': True}) }}" +""" + +RETURN = """ +_list: + description: list of subelements extracted +""" + +from ansible.errors import AnsibleError +from ansible.module_utils.six import string_types +from ansible.module_utils.parsing.convert_bool import boolean +from ansible.plugins.lookup import LookupBase +from ansible.utils.listify import listify_lookup_plugin_terms + + +FLAGS = ('skip_missing',) + + +class LookupModule(LookupBase): + + def run(self, terms, variables, **kwargs): + + def _raise_terms_error(msg=""): + raise AnsibleError( + "subelements lookup expects a list of two or three items, " + msg) + + terms[0] = listify_lookup_plugin_terms(terms[0], templar=self._templar) + + # check lookup terms - check number of terms + if not isinstance(terms, list) or not 2 <= len(terms) <= 3: + _raise_terms_error() + + # first term should be a list (or dict), second a string holding the subkey + if not isinstance(terms[0], (list, dict)) or not isinstance(terms[1], string_types): + _raise_terms_error("first a dict or a list, second a string pointing to the subkey") + subelements = terms[1].split(".") + + if isinstance(terms[0], dict): # convert to list: + if terms[0].get('skipped', False) is not False: + # the registered result was completely skipped + return [] + elementlist = [] + for key in terms[0]: + elementlist.append(terms[0][key]) + else: + elementlist = terms[0] + + # check for optional flags in third term + flags = {} + if len(terms) == 3: + flags = terms[2] + if not isinstance(flags, dict) and not all(isinstance(key, string_types) and key in FLAGS for key in flags): + _raise_terms_error("the optional third item must be a dict with flags %s" % FLAGS) + + # build_items + ret = [] + for item0 in elementlist: + if not isinstance(item0, dict): + raise AnsibleError("subelements lookup expects a dictionary, got '%s'" % item0) + if item0.get('skipped', False) is not False: + # this particular item is to be skipped + continue + + skip_missing = boolean(flags.get('skip_missing', False), strict=False) + subvalue = item0 + lastsubkey = False + sublist = [] + for subkey in subelements: + if subkey == subelements[-1]: + lastsubkey = True + if subkey not in subvalue: + if skip_missing: + continue + else: + raise AnsibleError("could not find '%s' key in iterated item '%s'" % (subkey, subvalue)) + if not lastsubkey: + if not isinstance(subvalue[subkey], dict): + if skip_missing: + continue + else: + raise AnsibleError("the key %s should point to a dictionary, got '%s'" % (subkey, subvalue[subkey])) + else: + subvalue = subvalue[subkey] + else: # lastsubkey + if not isinstance(subvalue[subkey], list): + raise AnsibleError("the key %s should point to a list, got '%s'" % (subkey, subvalue[subkey])) + else: + sublist = subvalue.pop(subkey, []) + for item1 in sublist: + ret.append((item0, item1)) + + return ret diff --git a/lib/ansible/plugins/lookup/template.py b/lib/ansible/plugins/lookup/template.py new file mode 100644 index 0000000..9c575b5 --- /dev/null +++ b/lib/ansible/plugins/lookup/template.py @@ -0,0 +1,165 @@ +# Copyright: (c) 2012, Michael DeHaan +# Copyright: (c) 2012-17, Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = """ + name: template + author: Michael DeHaan + version_added: "0.9" + short_description: retrieve contents of file after templating with Jinja2 + description: + - Returns a list of strings; for each template in the list of templates you pass in, returns a string containing the results of processing that template. + options: + _terms: + description: list of files to template + convert_data: + type: bool + description: + - Whether to convert YAML into data. If False, strings that are YAML will be left untouched. + - Mutually exclusive with the jinja2_native option. + default: true + variable_start_string: + description: The string marking the beginning of a print statement. + default: '{{' + version_added: '2.8' + type: str + variable_end_string: + description: The string marking the end of a print statement. + default: '}}' + version_added: '2.8' + type: str + jinja2_native: + description: + - Controls whether to use Jinja2 native types. + - It is off by default even if global jinja2_native is True. + - Has no effect if global jinja2_native is False. + - This offers more flexibility than the template module which does not use Jinja2 native types at all. + - Mutually exclusive with the convert_data option. + default: False + version_added: '2.11' + type: bool + template_vars: + description: A dictionary, the keys become additional variables available for templating. + default: {} + version_added: '2.3' + type: dict + comment_start_string: + description: The string marking the beginning of a comment statement. + version_added: '2.12' + type: str + comment_end_string: + description: The string marking the end of a comment statement. + version_added: '2.12' + type: str +""" + +EXAMPLES = """ +- name: show templating results + ansible.builtin.debug: + msg: "{{ lookup('ansible.builtin.template', './some_template.j2') }}" + +- name: show templating results with different variable start and end string + ansible.builtin.debug: + msg: "{{ lookup('ansible.builtin.template', './some_template.j2', variable_start_string='[%', variable_end_string='%]') }}" + +- name: show templating results with different comment start and end string + ansible.builtin.debug: + msg: "{{ lookup('ansible.builtin.template', './some_template.j2', comment_start_string='[#', comment_end_string='#]') }}" +""" + +RETURN = """ +_raw: + description: file(s) content after templating + type: list + elements: raw +""" + +from copy import deepcopy +import os + +import ansible.constants as C + +from ansible.errors import AnsibleError +from ansible.plugins.lookup import LookupBase +from ansible.module_utils._text import to_bytes, to_text +from ansible.template import generate_ansible_template_vars, AnsibleEnvironment +from ansible.utils.display import Display +from ansible.utils.native_jinja import NativeJinjaText + + +display = Display() + + +class LookupModule(LookupBase): + + def run(self, terms, variables, **kwargs): + + ret = [] + + self.set_options(var_options=variables, direct=kwargs) + + # capture options + convert_data_p = self.get_option('convert_data') + lookup_template_vars = self.get_option('template_vars') + jinja2_native = self.get_option('jinja2_native') and C.DEFAULT_JINJA2_NATIVE + variable_start_string = self.get_option('variable_start_string') + variable_end_string = self.get_option('variable_end_string') + comment_start_string = self.get_option('comment_start_string') + comment_end_string = self.get_option('comment_end_string') + + if jinja2_native: + templar = self._templar + else: + templar = self._templar.copy_with_new_env(environment_class=AnsibleEnvironment) + + for term in terms: + display.debug("File lookup term: %s" % term) + + lookupfile = self.find_file_in_search_path(variables, 'templates', term) + display.vvvv("File lookup using %s as file" % lookupfile) + if lookupfile: + b_template_data, show_data = self._loader._get_file_contents(lookupfile) + template_data = to_text(b_template_data, errors='surrogate_or_strict') + + # set jinja2 internal search path for includes + searchpath = variables.get('ansible_search_path', []) + if searchpath: + # our search paths aren't actually the proper ones for jinja includes. + # We want to search into the 'templates' subdir of each search path in + # addition to our original search paths. + newsearchpath = [] + for p in searchpath: + newsearchpath.append(os.path.join(p, 'templates')) + newsearchpath.append(p) + searchpath = newsearchpath + searchpath.insert(0, os.path.dirname(lookupfile)) + + # The template will have access to all existing variables, + # plus some added by ansible (e.g., template_{path,mtime}), + # plus anything passed to the lookup with the template_vars= + # argument. + vars = deepcopy(variables) + vars.update(generate_ansible_template_vars(term, lookupfile)) + vars.update(lookup_template_vars) + + with templar.set_temporary_context(variable_start_string=variable_start_string, + variable_end_string=variable_end_string, + comment_start_string=comment_start_string, + comment_end_string=comment_end_string, + available_variables=vars, searchpath=searchpath): + res = templar.template(template_data, preserve_trailing_newlines=True, + convert_data=convert_data_p, escape_backslashes=False) + + if (C.DEFAULT_JINJA2_NATIVE and not jinja2_native) or not convert_data_p: + # jinja2_native is true globally but off for the lookup, we need this text + # not to be processed by literal_eval anywhere in Ansible + res = NativeJinjaText(res) + + ret.append(res) + else: + raise AnsibleError("the template file %s could not be found for the lookup" % term) + + return ret diff --git a/lib/ansible/plugins/lookup/together.py b/lib/ansible/plugins/lookup/together.py new file mode 100644 index 0000000..c990e06 --- /dev/null +++ b/lib/ansible/plugins/lookup/together.py @@ -0,0 +1,68 @@ +# (c) 2013, Bradley Young +# (c) 2012-17 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = """ + name: together + author: Bradley Young (!UNKNOWN) + version_added: '1.3' + short_description: merges lists into synchronized list + description: + - Creates a list with the iterated elements of the supplied lists + - "To clarify with an example, [ 'a', 'b' ] and [ 1, 2 ] turn into [ ('a',1), ('b', 2) ]" + - This is basically the same as the 'zip_longest' filter and Python function + - Any 'unbalanced' elements will be substituted with 'None' + options: + _terms: + description: list of lists to merge + required: True +""" + +EXAMPLES = """ +- name: item.0 returns from the 'a' list, item.1 returns from the '1' list + ansible.builtin.debug: + msg: "{{ item.0 }} and {{ item.1 }}" + with_together: + - ['a', 'b', 'c', 'd'] + - [1, 2, 3, 4] +""" + +RETURN = """ + _list: + description: synchronized list + type: list + elements: list +""" +import itertools + +from ansible.errors import AnsibleError +from ansible.plugins.lookup import LookupBase +from ansible.utils.listify import listify_lookup_plugin_terms + + +class LookupModule(LookupBase): + """ + Transpose a list of arrays: + [1, 2, 3], [4, 5, 6] -> [1, 4], [2, 5], [3, 6] + Replace any empty spots in 2nd array with None: + [1, 2], [3] -> [1, 3], [2, None] + """ + + def _lookup_variables(self, terms): + results = [] + for x in terms: + intermediate = listify_lookup_plugin_terms(x, templar=self._templar) + results.append(intermediate) + return results + + def run(self, terms, variables=None, **kwargs): + + terms = self._lookup_variables(terms) + + my_list = terms[:] + if len(my_list) == 0: + raise AnsibleError("with_together requires at least one element in each list") + + return [self._flatten(x) for x in itertools.zip_longest(*my_list, fillvalue=None)] diff --git a/lib/ansible/plugins/lookup/unvault.py b/lib/ansible/plugins/lookup/unvault.py new file mode 100644 index 0000000..a9b7168 --- /dev/null +++ b/lib/ansible/plugins/lookup/unvault.py @@ -0,0 +1,63 @@ +# (c) 2020 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = """ + name: unvault + author: Ansible Core Team + version_added: "2.10" + short_description: read vaulted file(s) contents + description: + - This lookup returns the contents from vaulted (or not) file(s) on the Ansible controller's file system. + options: + _terms: + description: path(s) of files to read + required: True + notes: + - This lookup does not understand 'globbing' nor shell environment variables. +""" + +EXAMPLES = """ +- ansible.builtin.debug: msg="the value of foo.txt is {{ lookup('ansible.builtin.unvault', '/etc/foo.txt') | string | trim }}" +""" + +RETURN = """ + _raw: + description: + - content of file(s) as bytes + type: list + elements: raw +""" + +from ansible.errors import AnsibleParserError +from ansible.plugins.lookup import LookupBase +from ansible.module_utils._text import to_text +from ansible.utils.display import Display + +display = Display() + + +class LookupModule(LookupBase): + + def run(self, terms, variables=None, **kwargs): + + ret = [] + + self.set_options(var_options=variables, direct=kwargs) + + for term in terms: + display.debug("Unvault lookup term: %s" % term) + + # Find the file in the expected search path + lookupfile = self.find_file_in_search_path(variables, 'files', term) + display.vvvv(u"Unvault lookup found %s" % lookupfile) + if lookupfile: + actual_file = self._loader.get_real_file(lookupfile, decrypt=True) + with open(actual_file, 'rb') as f: + b_contents = f.read() + ret.append(to_text(b_contents)) + else: + raise AnsibleParserError('Unable to find file matching "%s" ' % term) + + return ret diff --git a/lib/ansible/plugins/lookup/url.py b/lib/ansible/plugins/lookup/url.py new file mode 100644 index 0000000..6790e1c --- /dev/null +++ b/lib/ansible/plugins/lookup/url.py @@ -0,0 +1,264 @@ +# (c) 2015, Brian Coca +# (c) 2012-17 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = """ +name: url +author: Brian Coca (@bcoca) +version_added: "1.9" +short_description: return contents from URL +description: + - Returns the content of the URL requested to be used as data in play. +options: + _terms: + description: urls to query + validate_certs: + description: Flag to control SSL certificate validation + type: boolean + default: True + split_lines: + description: Flag to control if content is returned as a list of lines or as a single text blob + type: boolean + default: True + use_proxy: + description: Flag to control if the lookup will observe HTTP proxy environment variables when present. + type: boolean + default: True + username: + description: Username to use for HTTP authentication. + type: string + version_added: "2.8" + password: + description: Password to use for HTTP authentication. + type: string + version_added: "2.8" + headers: + description: HTTP request headers + type: dictionary + default: {} + version_added: "2.9" + force: + description: Whether or not to set "cache-control" header with value "no-cache" + type: boolean + version_added: "2.10" + default: False + vars: + - name: ansible_lookup_url_force + env: + - name: ANSIBLE_LOOKUP_URL_FORCE + ini: + - section: url_lookup + key: force + timeout: + description: How long to wait for the server to send data before giving up + type: float + version_added: "2.10" + default: 10 + vars: + - name: ansible_lookup_url_timeout + env: + - name: ANSIBLE_LOOKUP_URL_TIMEOUT + ini: + - section: url_lookup + key: timeout + http_agent: + description: User-Agent to use in the request. The default was changed in 2.11 to C(ansible-httpget). + type: string + version_added: "2.10" + default: ansible-httpget + vars: + - name: ansible_lookup_url_agent + env: + - name: ANSIBLE_LOOKUP_URL_AGENT + ini: + - section: url_lookup + key: agent + force_basic_auth: + description: Force basic authentication + type: boolean + version_added: "2.10" + default: False + vars: + - name: ansible_lookup_url_agent + env: + - name: ANSIBLE_LOOKUP_URL_AGENT + ini: + - section: url_lookup + key: agent + follow_redirects: + description: String of urllib2, all/yes, safe, none to determine how redirects are followed, see RedirectHandlerFactory for more information + type: string + version_added: "2.10" + default: 'urllib2' + vars: + - name: ansible_lookup_url_follow_redirects + env: + - name: ANSIBLE_LOOKUP_URL_FOLLOW_REDIRECTS + ini: + - section: url_lookup + key: follow_redirects + use_gssapi: + description: + - Use GSSAPI handler of requests + - As of Ansible 2.11, GSSAPI credentials can be specified with I(username) and I(password). + type: boolean + version_added: "2.10" + default: False + vars: + - name: ansible_lookup_url_use_gssapi + env: + - name: ANSIBLE_LOOKUP_URL_USE_GSSAPI + ini: + - section: url_lookup + key: use_gssapi + use_netrc: + description: + - Determining whether to use credentials from ``~/.netrc`` file + - By default .netrc is used with Basic authentication headers + - When set to False, .netrc credentials are ignored + type: boolean + version_added: "2.14" + default: True + vars: + - name: ansible_lookup_url_use_netrc + env: + - name: ANSIBLE_LOOKUP_URL_USE_NETRC + ini: + - section: url_lookup + key: use_netrc + unix_socket: + description: String of file system path to unix socket file to use when establishing connection to the provided url + type: string + version_added: "2.10" + vars: + - name: ansible_lookup_url_unix_socket + env: + - name: ANSIBLE_LOOKUP_URL_UNIX_SOCKET + ini: + - section: url_lookup + key: unix_socket + ca_path: + description: String of file system path to CA cert bundle to use + type: string + version_added: "2.10" + vars: + - name: ansible_lookup_url_ca_path + env: + - name: ANSIBLE_LOOKUP_URL_CA_PATH + ini: + - section: url_lookup + key: ca_path + unredirected_headers: + description: A list of headers to not attach on a redirected request + type: list + elements: string + version_added: "2.10" + vars: + - name: ansible_lookup_url_unredir_headers + env: + - name: ANSIBLE_LOOKUP_URL_UNREDIR_HEADERS + ini: + - section: url_lookup + key: unredirected_headers + ciphers: + description: + - SSL/TLS Ciphers to use for the request + - 'When a list is provided, all ciphers are joined in order with C(:)' + - See the L(OpenSSL Cipher List Format,https://www.openssl.org/docs/manmaster/man1/openssl-ciphers.html#CIPHER-LIST-FORMAT) + for more details. + - The available ciphers is dependent on the Python and OpenSSL/LibreSSL versions + type: list + elements: string + version_added: '2.14' + vars: + - name: ansible_lookup_url_ciphers + env: + - name: ANSIBLE_LOOKUP_URL_CIPHERS + ini: + - section: url_lookup + key: ciphers +""" + +EXAMPLES = """ +- name: url lookup splits lines by default + ansible.builtin.debug: msg="{{item}}" + loop: "{{ lookup('ansible.builtin.url', 'https://github.com/gremlin.keys', wantlist=True) }}" + +- name: display ip ranges + ansible.builtin.debug: msg="{{ lookup('ansible.builtin.url', 'https://ip-ranges.amazonaws.com/ip-ranges.json', split_lines=False) }}" + +- name: url lookup using authentication + ansible.builtin.debug: msg="{{ lookup('ansible.builtin.url', 'https://some.private.site.com/file.txt', username='bob', password='hunter2') }}" + +- name: url lookup using basic authentication + ansible.builtin.debug: + msg: "{{ lookup('ansible.builtin.url', 'https://some.private.site.com/file.txt', username='bob', password='hunter2', force_basic_auth='True') }}" + +- name: url lookup using headers + ansible.builtin.debug: + msg: "{{ lookup('ansible.builtin.url', 'https://some.private.site.com/api/service', headers={'header1':'value1', 'header2':'value2'} ) }}" +""" + +RETURN = """ + _list: + description: list of list of lines or content of url(s) + type: list + elements: str +""" + +from urllib.error import HTTPError, URLError + +from ansible.errors import AnsibleError +from ansible.module_utils._text import to_text, to_native +from ansible.module_utils.urls import open_url, ConnectionError, SSLValidationError +from ansible.plugins.lookup import LookupBase +from ansible.utils.display import Display + +display = Display() + + +class LookupModule(LookupBase): + + def run(self, terms, variables=None, **kwargs): + + self.set_options(var_options=variables, direct=kwargs) + + ret = [] + for term in terms: + display.vvvv("url lookup connecting to %s" % term) + try: + response = open_url( + term, validate_certs=self.get_option('validate_certs'), + use_proxy=self.get_option('use_proxy'), + url_username=self.get_option('username'), + url_password=self.get_option('password'), + headers=self.get_option('headers'), + force=self.get_option('force'), + timeout=self.get_option('timeout'), + http_agent=self.get_option('http_agent'), + force_basic_auth=self.get_option('force_basic_auth'), + follow_redirects=self.get_option('follow_redirects'), + use_gssapi=self.get_option('use_gssapi'), + unix_socket=self.get_option('unix_socket'), + ca_path=self.get_option('ca_path'), + unredirected_headers=self.get_option('unredirected_headers'), + ciphers=self.get_option('ciphers'), + use_netrc=self.get_option('use_netrc') + ) + except HTTPError as e: + raise AnsibleError("Received HTTP error for %s : %s" % (term, to_native(e))) + except URLError as e: + raise AnsibleError("Failed lookup url for %s : %s" % (term, to_native(e))) + except SSLValidationError as e: + raise AnsibleError("Error validating the server's certificate for %s: %s" % (term, to_native(e))) + except ConnectionError as e: + raise AnsibleError("Error connecting to %s: %s" % (term, to_native(e))) + + if self.get_option('split_lines'): + for line in response.read().splitlines(): + ret.append(to_text(line)) + else: + ret.append(to_text(response.read())) + return ret diff --git a/lib/ansible/plugins/lookup/varnames.py b/lib/ansible/plugins/lookup/varnames.py new file mode 100644 index 0000000..442b81b --- /dev/null +++ b/lib/ansible/plugins/lookup/varnames.py @@ -0,0 +1,79 @@ +# (c) 2017 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = """ + name: varnames + author: Ansible Core Team + version_added: "2.8" + short_description: Lookup matching variable names + description: + - Retrieves a list of matching Ansible variable names. + options: + _terms: + description: List of Python regex patterns to search for in variable names. + required: True +""" + +EXAMPLES = """ +- name: List variables that start with qz_ + ansible.builtin.debug: msg="{{ lookup('ansible.builtin.varnames', '^qz_.+')}}" + vars: + qz_1: hello + qz_2: world + qa_1: "I won't show" + qz_: "I won't show either" + +- name: Show all variables + ansible.builtin.debug: msg="{{ lookup('ansible.builtin.varnames', '.+')}}" + +- name: Show variables with 'hosts' in their names + ansible.builtin.debug: msg="{{ lookup('ansible.builtin.varnames', 'hosts')}}" + +- name: Find several related variables that end specific way + ansible.builtin.debug: msg="{{ lookup('ansible.builtin.varnames', '.+_zone$', '.+_location$') }}" + +""" + +RETURN = """ +_value: + description: + - List of the variable names requested. + type: list +""" + +import re + +from ansible.errors import AnsibleError +from ansible.module_utils._text import to_native +from ansible.module_utils.six import string_types +from ansible.plugins.lookup import LookupBase + + +class LookupModule(LookupBase): + + def run(self, terms, variables=None, **kwargs): + + if variables is None: + raise AnsibleError('No variables available to search') + + self.set_options(var_options=variables, direct=kwargs) + + ret = [] + variable_names = list(variables.keys()) + for term in terms: + + if not isinstance(term, string_types): + raise AnsibleError('Invalid setting identifier, "%s" is not a string, it is a %s' % (term, type(term))) + + try: + name = re.compile(term) + except Exception as e: + raise AnsibleError('Unable to use "%s" as a search parameter: %s' % (term, to_native(e))) + + for varname in variable_names: + if name.search(varname): + ret.append(varname) + + return ret diff --git a/lib/ansible/plugins/lookup/vars.py b/lib/ansible/plugins/lookup/vars.py new file mode 100644 index 0000000..dd5f763 --- /dev/null +++ b/lib/ansible/plugins/lookup/vars.py @@ -0,0 +1,106 @@ +# (c) 2017 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +DOCUMENTATION = """ + name: vars + author: Ansible Core Team + version_added: "2.5" + short_description: Lookup templated value of variables + description: + - 'Retrieves the value of an Ansible variable. Note: Only returns top level variable names.' + options: + _terms: + description: The variable names to look up. + required: True + default: + description: + - What to return if a variable is undefined. + - If no default is set, it will result in an error if any of the variables is undefined. +""" + +EXAMPLES = """ +- name: Show value of 'variablename' + ansible.builtin.debug: msg="{{ lookup('ansible.builtin.vars', 'variabl' + myvar) }}" + vars: + variablename: hello + myvar: ename + +- name: Show default empty since i dont have 'variablnotename' + ansible.builtin.debug: msg="{{ lookup('ansible.builtin.vars', 'variabl' + myvar, default='')}}" + vars: + variablename: hello + myvar: notename + +- name: Produce an error since i dont have 'variablnotename' + ansible.builtin.debug: msg="{{ lookup('ansible.builtin.vars', 'variabl' + myvar)}}" + ignore_errors: True + vars: + variablename: hello + myvar: notename + +- name: find several related variables + ansible.builtin.debug: msg="{{ lookup('ansible.builtin.vars', 'ansible_play_hosts', 'ansible_play_batch', 'ansible_play_hosts_all') }}" + +- name: Access nested variables + ansible.builtin.debug: msg="{{ lookup('ansible.builtin.vars', 'variabl' + myvar).sub_var }}" + ignore_errors: True + vars: + variablename: + sub_var: 12 + myvar: ename + +- name: alternate way to find some 'prefixed vars' in loop + ansible.builtin.debug: msg="{{ lookup('ansible.builtin.vars', 'ansible_play_' + item) }}" + loop: + - hosts + - batch + - hosts_all +""" + +RETURN = """ +_value: + description: + - value of the variables requested. + type: list + elements: raw +""" + +from ansible.errors import AnsibleError, AnsibleUndefinedVariable +from ansible.module_utils.six import string_types +from ansible.plugins.lookup import LookupBase + + +class LookupModule(LookupBase): + + def run(self, terms, variables=None, **kwargs): + if variables is not None: + self._templar.available_variables = variables + myvars = getattr(self._templar, '_available_variables', {}) + + self.set_options(var_options=variables, direct=kwargs) + default = self.get_option('default') + + ret = [] + for term in terms: + if not isinstance(term, string_types): + raise AnsibleError('Invalid setting identifier, "%s" is not a string, its a %s' % (term, type(term))) + + try: + try: + value = myvars[term] + except KeyError: + try: + value = myvars['hostvars'][myvars['inventory_hostname']][term] + except KeyError: + raise AnsibleUndefinedVariable('No variable found with this name: %s' % term) + + ret.append(self._templar.template(value, fail_on_undefined=True)) + except AnsibleUndefinedVariable: + if default is not None: + ret.append(default) + else: + raise + + return ret -- cgit v1.2.3