ansible/ansible
Lookup plugins
Lookups are controller-side functions that produce data into the variable namespace. They're invoked as {{ lookup('name', ...) }} (or with the shorter alias {{ q('name', ...) }}). Used to read files, environment variables, vault contents, command output, password files, or anything else the controller can produce.
Where they live
lib/ansible/plugins/lookup/
├── __init__.py
├── config.py
├── csvfile.py
├── dict.py
├── env.py
├── file.py
├── fileglob.py
├── first_found.py
├── indexed_items.py
├── ini.py
├── ... (about 40 in total)A non-exhaustive list of built-ins: config, csvfile, dict, env, file, fileglob, first_found, indexed_items, ini, items, lines, list, nested, password, pipe, random_choice, sequence, subelements, template, together, unvault, url, vars, with_dict, with_first_found, with_items, etc.
Lookups vs. filters
- A filter transforms an existing value:
{{ value | upper }}. - A lookup produces data:
{{ lookup('file', '/etc/hosts') }}.
The distinction is functional: lookups can read files, run subprocesses, hit the network. Filters should not.
Lookup vs. with_
Old syntax loops used with_<lookup> directly:
- debug: msg={{ item }}
with_items: '{{ my_list }}'Modern form uses loop: with lookup or just bare data:
- debug: msg={{ item }}
loop: '{{ my_list }}'Both still work, but with_* keywords are essentially aliases for loop: "{{ lookup('<name>', ...) }}" with a wantlist flag.
LookupBase
lib/ansible/plugins/lookup/__init__.py:LookupBase is the parent. The contract:
class LookupModule(LookupBase):
def run(self, terms, variables=None, **kwargs):
# terms: the positional args, always a list (the first one is normally the "input")
# variables: the current variable scope
# kwargs: keyword arguments parsed from k=v style
return [list_of_results]The return value must be a list even for "single-value" lookups (the result of lookup('env', 'HOME') is [home_path], and the templating layer unwraps the single-element list). Use wantlist=True to keep it as a list when the user expects multiple results.
The base provides:
_loader— aDataLoaderfor reading files relative to the playbook.find_file_in_search_path(...)— locatefiles/foo.txtcorrectly in role search order.get_basedir(variables)— the playbook root for relative paths.set_options(...)andget_option(...)— for plugin options.
Common patterns
file lookup
- debug: msg="{{ lookup('file', '/etc/hostname') }}"lib/ansible/plugins/lookup/file.py reads the file with the dataloader (so vault-encrypted files decrypt automatically). Returns the file's text content.
env lookup
- debug: msg="API key starts with {{ lookup('env', 'API_KEY')[:8] }}"lib/ansible/plugins/lookup/env.py returns os.environ.get(name, ''). Note this reads the controller's environment, not the target's.
pipe lookup
- name: who am I locally
debug: msg="{{ lookup('pipe', 'whoami') }}"Runs the command on the controller and returns stdout. Used for "fetch this small piece of dynamic data" cases.
password lookup
- name: ensure user has a password
user:
name: alice
password: "{{ lookup('password', '/path/to/password.txt length=20 chars=ascii_letters') }}"Reads the file if it exists, generates a new password if not. Stable across runs — the lookup file is the source of truth.
template lookup
- debug: msg="{{ lookup('template', 'config.yml.j2') }}"Renders a Jinja template against the current variable scope and returns the result. Used for inlining computed config snippets.
first_found
- copy:
src: "{{ lookup('first_found', files) }}"
dest: /etc/foo.conf
vars:
files:
- 'config.{{ ansible_distribution }}.conf'
- 'config.default.conf'Walk a list of paths and return the first one that exists. Bigger than it sounds — the first_found plugin handles the role search path semantics.
unvault
lib/ansible/plugins/lookup/unvault.py decrypts a vault file and returns its contents. Used when you want to read encrypted credentials inline rather than putting them in group_vars/all/vault.yml.
How lookups integrate with templating
graph LR
EXPR["{{ lookup('file', path) }}"] --> JINJA[AnsibleEnvironment]
JINJA --> INVOKE[_invoke_lookup<br/>_internal/_templating/_jinja_plugins.py]
INVOKE --> LOAD[lookup_loader.get name]
LOAD --> CLASS[LookupModule]
CLASS --> RUN[run(terms, variables)]
RUN --> RESULT[Return list]
RESULT --> UNWRAP{wantlist?}
UNWRAP -->|no| ELEM[Return first element]
UNWRAP -->|yes| LIST[Return entire list]The lookup runs synchronously inside the templating evaluation. Slow lookups slow down templating, which slows down task dispatch. Cache aggressively: lookup('url', ...) makes a fresh HTTP call every time, but you can wrap it in set_fact once.
Errors and undefined
Lookups can fail. Standard convention:
- A required term that's missing →
raise AnsibleError("..."). - A term that can't be found but is okay to skip → return
[]. - A connection or permission error → raise.
Errors propagate up through Jinja and surface as a templating error with the lookup name attached.
Integration points
- Loaded by:
lookup_loaderinlib/ansible/plugins/loader.py. - Invoked from:
lib/ansible/_internal/_templating/_jinja_plugins.py:_invoke_lookup. - Used by: any Jinja expression in a playbook, vars file, or template.
Entry points for modification
- A new lookup — write a plugin in a collection. Subclass
LookupBase. Implementrun(). Declare options inDOCUMENTATION. - Adding
q()/query()syntax — already done;qis registered as an alias by_jinja_plugins.py. Don't add a new top-level alias without consensus.
Cross-links
- Filter plugins — for value transformations.
- Templating — the engine that calls lookups.
Built by Factory AutoWiki from public repository content. It is a generated preview for codebase exploration, not source-maintained documentation.