mirror of
https://github.com/ansible-collections/community.general.git
synced 2024-09-14 20:13:21 +02:00
move from with_<lookup>: to loop:
- old functionality is still available direct lookup use, the following are equivalent with_nested: [[1,2,3], ['a','b','c']] loop: "{{lookup('nested', [1,2,3], ['a','b','c'])}}" - avoid squashing with 'loop:' - fixed test to use new intenal attributes - removed most of 'lookup docs' as these now reside in the plugins
This commit is contained in:
parent
bd17edd5ed
commit
d84df2405d
24 changed files with 157 additions and 1211 deletions
|
@ -7,8 +7,12 @@ Ansible Changes By Release
|
|||
|
||||
### Major Changes
|
||||
* Removed the previously deprecated 'accelerate' mode and all associated keywords and code.
|
||||
* Removed the previouslly deprecated 'accelerate' mode and all associated keywords and code.
|
||||
* New simpler and more intuitive 'loop' keyword for task loops
|
||||
|
||||
### Deprecations
|
||||
* previouslly deprecated 'hostfile' config settings have been 're-deprecated' as previouslly code did not warn about deprecated configuration settings.
|
||||
* The ``with_<lookup>`` loops are deprecated in favor of the new ``loop`` keyword
|
||||
|
||||
#### Deprecated Modules (to be removed in 2.9):
|
||||
|
||||
|
|
|
@ -49,14 +49,15 @@ for aclass in class_list:
|
|||
|
||||
# loop is really with_ for users
|
||||
if name == 'Task':
|
||||
oblist[name]['with_<lookup_plugin>'] = 'with_ is how loops are defined, it can use any available lookup plugin to generate the item list'
|
||||
oblist[name]['with_<lookup_plugin>'] = 'DEPRECATED: use ``loop`` instead, with_ used to be how loops were defined, '
|
||||
'it can use any available lookup plugin to generate the item list'
|
||||
|
||||
# local_action is implicit with action
|
||||
if 'action' in oblist[name]:
|
||||
oblist[name]['local_action'] = 'Same as action but also implies ``delegate_to: localhost``'
|
||||
|
||||
# remove unusable (used to be private?)
|
||||
for nouse in ('loop', 'loop_args'):
|
||||
for nouse in ('loop_args'):
|
||||
if nouse in oblist[name]:
|
||||
del oblist[name][nouse]
|
||||
|
||||
|
|
|
@ -29,6 +29,7 @@ gather_timeout: Allows you to set the timeout for the fact gathering plugin cont
|
|||
handlers: "A section with tasks that are treated as handlers, these won't get executed normally, only when notified. After each section of tasks is complete."
|
||||
hosts: "A list of groups, hosts or host pattern that translates into a list of hosts that are the play's target."
|
||||
ignore_errors: Boolean that allows you to ignore task failures and continue with play. It does not affect connection errors.
|
||||
loop: "Takes a list for the task to iterate over, saving each list element into the ``item`` variable (configurable via loop_control)"
|
||||
loop_control: "Several keys here allow you to modify/set loop behaviour in a task see http://docs.ansible.com/ansible/latest/playbooks_loops.html#loop-control for details."
|
||||
max_fail_percentage: can be used to abort the run after a given percentage of hosts in the current batch has failed.
|
||||
name: "It's a name, works mostly for documentation, in the case of tasks/handlers it can be an identifier."
|
||||
|
|
|
@ -378,7 +378,7 @@ A steadfast rule is 'always use {{ }} except when `when:`'.
|
|||
Conditionals are always run through Jinja2 as to resolve the expression,
|
||||
so `when:`, `failed_when:` and `changed_when:` are always templated and you should avoid adding `{{}}`.
|
||||
|
||||
In most other cases you should always use the brackets, even if previously you could use variables without specifying (like `with_` clauses),
|
||||
In most other cases you should always use the brackets, even if previously you could use variables without specifying (like `loop` or `with_` clauses),
|
||||
as this made it hard to distinguish between an undefined variable and a string.
|
||||
|
||||
Another rule is 'moustaches don't stack'. We often see this:
|
||||
|
|
|
@ -255,22 +255,22 @@ when a term comes up on the mailing list.
|
|||
that we are managing the local host and not a remote machine.
|
||||
|
||||
Lookup Plugin
|
||||
A lookup plugin is a way to get data into Ansible from the outside
|
||||
world. These are how such things as ``with_items``, a basic looping
|
||||
plugin, are implemented. There are also lookup plugins like
|
||||
``with_file`` which load data from a file and ones for querying
|
||||
environment variables, DNS text records, or key value stores. Lookup
|
||||
plugins can also be accessed in templates, e.g.,
|
||||
A lookup plugin is a way to get data into Ansible from the outside world.
|
||||
Lookup plugins are an extension of Jinja2 and can be accessed in templates, e.g.,
|
||||
``{{ lookup('file','/path/to/file') }}``.
|
||||
These are how such things as ``with_items``, are implemented.
|
||||
There are also lookup plugins like ``file`` which loads data from
|
||||
a file and ones for querying environment variables, DNS text records,
|
||||
or key value stores.
|
||||
|
||||
Loops
|
||||
Generally, Ansible is not a programming language. It prefers to be
|
||||
more declarative, though various constructs like ``with_items`` allow
|
||||
more declarative, though various constructs like ``loop`` allow
|
||||
a particular task to be repeated for multiple items in a list.
|
||||
Certain modules, like :ref:`yum <yum>` and :ref:`apt <apt>`, are actually
|
||||
optimized for this, and can install all packages given in those lists
|
||||
Certain modules, like :ref:`yum <yum>` and :ref:`apt <apt>`, actually take
|
||||
lists directly, and can install all packages given in those lists
|
||||
within a single transaction, dramatically speeding up total time to
|
||||
configuration.
|
||||
configuration, so they can be used without loops.
|
||||
|
||||
Modules
|
||||
Modules are the units of work that Ansible ships out to remote
|
||||
|
|
|
@ -115,7 +115,7 @@ From this, we'll use the add_host module to dynamically create a host group cons
|
|||
|
||||
- name: Add all instance public IPs to host group
|
||||
add_host: hostname={{ item.public_ip }} groups=ec2hosts
|
||||
with_items: "{{ ec2.instances }}"
|
||||
loop: "{{ ec2.instances }}"
|
||||
|
||||
With the host group now created, a second play at the bottom of the same provisioning playbook file might now have some configuration steps::
|
||||
|
||||
|
|
|
@ -125,7 +125,7 @@ Or by looping over a regions list if you want to do the task in every region:
|
|||
name: my-ssh-key
|
||||
public_key: "{{ lookup('file', '~/.ssh/id_rsa.pub') }}"
|
||||
api_region: "{{ item }}"
|
||||
with_items:
|
||||
loop:
|
||||
- exoscale
|
||||
- exmaple_cloud_one
|
||||
- exmaple_cloud_two
|
||||
|
@ -255,7 +255,7 @@ Now to the fun part. We create a playbook to create our infrastructure we call i
|
|||
ip_address: "{{ public_ip }}"
|
||||
port: "{{ item.port }}"
|
||||
cidr: "{{ item.cidr | default('0.0.0.0/0') }}"
|
||||
with_items: "{{ cs_firewall }}"
|
||||
loop: "{{ cs_firewall }}"
|
||||
when: public_ip is defined
|
||||
|
||||
- name: ensure static NATs
|
||||
|
@ -326,7 +326,7 @@ The playbook looks like the following:
|
|||
- name: ensure security groups exist
|
||||
cs_securitygroup:
|
||||
name: "{{ item }}"
|
||||
with_items:
|
||||
loop:
|
||||
- default
|
||||
- web
|
||||
|
||||
|
@ -335,7 +335,7 @@ The playbook looks like the following:
|
|||
security_group: default
|
||||
start_port: "{{ item }}"
|
||||
end_port: "{{ item }}"
|
||||
with_items:
|
||||
loop:
|
||||
- 22
|
||||
|
||||
- name: add inbound TCP rules to security group web
|
||||
|
@ -343,7 +343,7 @@ The playbook looks like the following:
|
|||
security_group: web
|
||||
start_port: "{{ item }}"
|
||||
end_port: "{{ item }}"
|
||||
with_items:
|
||||
loop:
|
||||
- 80
|
||||
- 443
|
||||
|
||||
|
|
|
@ -213,11 +213,11 @@ A playbook would looks like this:
|
|||
|
||||
- name: Wait for SSH to come up
|
||||
wait_for: host={{ item.public_ip }} port=22 delay=10 timeout=60
|
||||
with_items: "{{ gce.instance_data }}"
|
||||
loop: "{{ gce.instance_data }}"
|
||||
|
||||
- name: Add host to groupname
|
||||
add_host: hostname={{ item.public_ip }} groupname=new_instances
|
||||
with_items: "{{ gce.instance_data }}"
|
||||
loop: "{{ gce.instance_data }}"
|
||||
|
||||
- name: Manage new instances
|
||||
hosts: new_instances
|
||||
|
|
|
@ -178,7 +178,7 @@ The following playbook will create an SSH key, 3 Packet servers, and then wait u
|
|||
port: 22
|
||||
state: started
|
||||
timeout: 500
|
||||
with_items: "{{ newhosts.devices }}"
|
||||
loop: "{{ newhosts.devices }}"
|
||||
|
||||
|
||||
As with most Ansible modules, the default states of the Packet modules are idempotent, meaning the resources in your project will remain the same after re-runs of a playbook. Thus, we can keep the ``packet_sshkey`` module call in our playbook. If the public key is already in your Packet account, the call will have no effect.
|
||||
|
|
|
@ -134,7 +134,7 @@ The rax module returns data about the nodes it creates, like IP addresses, hostn
|
|||
ansible_host: "{{ item.rax_accessipv4 }}"
|
||||
ansible_ssh_pass: "{{ item.rax_adminpass }}"
|
||||
groups: raxhosts
|
||||
with_items: "{{ rax.success }}"
|
||||
loop: "{{ rax.success }}"
|
||||
when: rax.action == 'create'
|
||||
|
||||
With the host group now created, the next play in this playbook could now configure servers belonging to the raxhosts group.
|
||||
|
@ -522,7 +522,7 @@ Build a complete webserver environment with servers, custom networks and load ba
|
|||
ansible_ssh_pass: "{{ item.rax_adminpass }}"
|
||||
ansible_user: root
|
||||
groups: web
|
||||
with_items: "{{ rax.success }}"
|
||||
loop: "{{ rax.success }}"
|
||||
when: rax.action == 'create'
|
||||
|
||||
- name: Add servers to Load balancer
|
||||
|
@ -536,7 +536,7 @@ Build a complete webserver environment with servers, custom networks and load ba
|
|||
type: primary
|
||||
wait: yes
|
||||
region: IAD
|
||||
with_items: "{{ rax.success }}"
|
||||
loop: "{{ rax.success }}"
|
||||
when: rax.action == 'create'
|
||||
|
||||
- name: Configure servers
|
||||
|
@ -608,7 +608,7 @@ Using a Control Machine
|
|||
ansible_user: root
|
||||
rax_id: "{{ item.rax_id }}"
|
||||
groups: web,new_web
|
||||
with_items: "{{ rax.success }}"
|
||||
loop: "{{ rax.success }}"
|
||||
when: rax.action == 'create'
|
||||
|
||||
- name: Wait for rackconnect and managed cloud automation to complete
|
||||
|
|
|
@ -213,16 +213,16 @@ Here is the next part of the update play::
|
|||
- name: disable nagios alerts for this host webserver service
|
||||
nagios: action=disable_alerts host={{ inventory_hostname }} services=webserver
|
||||
delegate_to: "{{ item }}"
|
||||
with_items: "{{ groups.monitoring }}"
|
||||
loop: "{{ groups.monitoring }}"
|
||||
|
||||
- name: disable the server in haproxy
|
||||
shell: echo "disable server myapplb/{{ inventory_hostname }}" | socat stdio /var/lib/haproxy/stats
|
||||
delegate_to: "{{ item }}"
|
||||
with_items: "{{ groups.lbservers }}"
|
||||
loop: "{{ groups.lbservers }}"
|
||||
|
||||
The ``pre_tasks`` keyword just lets you list tasks to run before the roles are called. This will make more sense in a minute. If you look at the names of these tasks, you can see that we are disabling Nagios alerts and then removing the webserver that we are currently updating from the HAProxy load balancing pool.
|
||||
|
||||
The ``delegate_to`` and ``with_items`` arguments, used together, cause Ansible to loop over each monitoring server and load balancer, and perform that operation (delegate that operation) on the monitoring or load balancing server, "on behalf" of the webserver. In programming terms, the outer loop is the list of web servers, and the inner loop is the list of monitoring servers.
|
||||
The ``delegate_to`` and ``loop`` arguments, used together, cause Ansible to loop over each monitoring server and load balancer, and perform that operation (delegate that operation) on the monitoring or load balancing server, "on behalf" of the webserver. In programming terms, the outer loop is the list of web servers, and the inner loop is the list of monitoring servers.
|
||||
|
||||
Note that the HAProxy step looks a little complicated. We're using HAProxy in this example because it's freely available, though if you have (for instance) an F5 or Netscaler in your infrastructure (or maybe you have an AWS Elastic IP setup?), you can use modules included in core Ansible to communicate with them instead. You might also wish to use other monitoring modules instead of nagios, but this just shows the main goal of the 'pre tasks' section -- take the server out of monitoring, and take it out of rotation.
|
||||
|
||||
|
@ -239,12 +239,12 @@ Finally, in the ``post_tasks`` section, we reverse the changes to the Nagios con
|
|||
- name: Enable the server in haproxy
|
||||
shell: echo "enable server myapplb/{{ inventory_hostname }}" | socat stdio /var/lib/haproxy/stats
|
||||
delegate_to: "{{ item }}"
|
||||
with_items: "{{ groups.lbservers }}"
|
||||
loop: "{{ groups.lbservers }}"
|
||||
|
||||
- name: re-enable nagios alerts
|
||||
nagios: action=enable_alerts host={{ inventory_hostname }} services=webserver
|
||||
delegate_to: "{{ item }}"
|
||||
with_items: "{{ groups.monitoring }}"
|
||||
loop: "{{ groups.monitoring }}"
|
||||
|
||||
Again, if you were using a Netscaler or F5 or Elastic Load Balancer, you would just substitute in the appropriate modules instead.
|
||||
|
||||
|
|
|
@ -95,7 +95,7 @@ of tasks running concurrently, you can do it this way::
|
|||
- 5
|
||||
durations: "{{ item }}"
|
||||
include_tasks: execute_batch.yml
|
||||
with_items:
|
||||
loop:
|
||||
- "{{ sleep_durations | batch(2) | list }}"
|
||||
|
||||
#####################
|
||||
|
@ -105,7 +105,7 @@ of tasks running concurrently, you can do it this way::
|
|||
command: sleep {{ async_item }}
|
||||
async: 45
|
||||
poll: 0
|
||||
with_items: "{{ durations }}"
|
||||
loop: "{{ durations }}"
|
||||
loop_control:
|
||||
loop_var: "async_item"
|
||||
register: async_results
|
||||
|
@ -113,7 +113,7 @@ of tasks running concurrently, you can do it this way::
|
|||
- name: Check sync status
|
||||
async_status:
|
||||
jid: "{{ async_result_item.ansible_job_id }}"
|
||||
with_items: "{{ async_results.results }}"
|
||||
loop: "{{ async_results.results }}"
|
||||
loop_control:
|
||||
loop_var: "async_result_item"
|
||||
register: async_poll_results
|
||||
|
|
|
@ -16,7 +16,7 @@ by the tasks enclosed by a block. i.e. a `when` will be applied to the tasks, no
|
|||
- name: Install Apache
|
||||
block:
|
||||
- yum: name={{ item }} state=installed
|
||||
with_items:
|
||||
loop:
|
||||
- httpd
|
||||
- memcached
|
||||
- template: src=templates/src.j2 dest=/etc/foo.conf
|
||||
|
|
|
@ -117,24 +117,24 @@ As the examples show, you don't need to use `{{ }}` to use variables inside cond
|
|||
|
||||
Loops and Conditionals
|
||||
``````````````````````
|
||||
Combining `when` with `with_items` (see :doc:`playbooks_loops`), be aware that the `when` statement is processed separately for each item. This is by design::
|
||||
Combining `when` with loops (see :doc:`playbooks_loops`), be aware that the `when` statement is processed separately for each item. This is by design::
|
||||
|
||||
tasks:
|
||||
- command: echo {{ item }}
|
||||
with_items: [ 0, 2, 4, 6, 8, 10 ]
|
||||
loop: [ 0, 2, 4, 6, 8, 10 ]
|
||||
when: item > 5
|
||||
|
||||
If you need to skip the whole task depending on the loop variable being defined, used the `|default` filter to provide an empty iterator::
|
||||
|
||||
- command: echo {{ item }}
|
||||
with_items: "{{ mylist|default([]) }}"
|
||||
loop: "{{ mylist|default([]) }}"
|
||||
when: item > 5
|
||||
|
||||
|
||||
If using `with_dict` which does not take a list::
|
||||
If using a dict in a loop::
|
||||
|
||||
- command: echo {{ item.key }}
|
||||
with_dict: "{{ mydict|default({}) }}"
|
||||
loop: "{{ lookup('dict', mydict|default({})) }}"
|
||||
when: item.value > 5
|
||||
|
||||
.. _loading_in_custom_facts:
|
||||
|
@ -259,13 +259,12 @@ The following example shows how to template out a configuration file that was ve
|
|||
|
||||
- name: template a file
|
||||
template: src={{ item }} dest=/etc/myapp/foo.conf
|
||||
with_first_found:
|
||||
- files:
|
||||
- {{ ansible_distribution }}.conf
|
||||
- default.conf
|
||||
paths:
|
||||
- search_location_one/somedir/
|
||||
- /opt/other_location/somedir/
|
||||
loop: "{{lookup('first_found', { 'files': myfiles, 'paths': mypaths})}}"
|
||||
vars:
|
||||
myfiles:
|
||||
- "{{ansible_distribution}}.conf"
|
||||
- default.conf
|
||||
mypaths: ['search_location_one/somedir/', '/opt/other_location/somedir/']
|
||||
|
||||
Register Variables
|
||||
``````````````````
|
||||
|
@ -288,14 +287,13 @@ The 'register' keyword decides what variable to save a result in. The resulting
|
|||
when: motd_contents.stdout.find('hi') != -1
|
||||
|
||||
As shown previously, the registered variable's string contents are accessible with the 'stdout' value.
|
||||
The registered result can be used in the "with_items" of a task if it is converted into
|
||||
The registered result can be used in the loop of a task if it is converted into
|
||||
a list (or already is a list) as shown below. "stdout_lines" is already available on the object as
|
||||
well though you could also call "home_dirs.stdout.split()" if you wanted, and could split by other
|
||||
fields::
|
||||
|
||||
- name: registered variable usage as a with_items list
|
||||
- name: registered variable usage as a loop list
|
||||
hosts: all
|
||||
|
||||
tasks:
|
||||
|
||||
- name: retrieve the list of home directories
|
||||
|
@ -304,8 +302,8 @@ fields::
|
|||
|
||||
- name: add home dirs to the backup spooler
|
||||
file: path=/mnt/bkspool/{{ item }} src=/home/{{ item }} state=link
|
||||
with_items: "{{ home_dirs.stdout_lines }}"
|
||||
# same as with_items: "{{ home_dirs.stdout.split() }}"
|
||||
loop: "{{ home_dirs.stdout_lines }}"
|
||||
# same as loop: "{{ home_dirs.stdout.split() }}"
|
||||
|
||||
As shown previously, the registered variable's string contents are accessible with the 'stdout' value.
|
||||
You may check the registered variable's string contents for emptiness::
|
||||
|
|
|
@ -175,7 +175,7 @@ In case you have to specify more arguments you can use the following syntax::
|
|||
to: "{{ mail_recipient }}"
|
||||
body: "{{ mail_body }}"
|
||||
run_once: True
|
||||
|
||||
|
||||
The `ansible_host` variable (`ansible_ssh_host` in 1.x or specific to ssh/paramiko plugins) reflects the host a task is delegated to.
|
||||
|
||||
.. _delegate_facts:
|
||||
|
@ -195,7 +195,7 @@ In 2.0, the directive `delegate_facts` may be set to `True` to assign the task's
|
|||
setup:
|
||||
delegate_to: "{{item}}"
|
||||
delegate_facts: True
|
||||
with_items: "{{groups['dbservers']}}"
|
||||
loop: "{{groups['dbservers']}}"
|
||||
|
||||
The above will gather facts for the machines in the dbservers group and assign the facts to those machines and not to app_servers.
|
||||
This way you can lookup `hostvars['dbhost1']['default_ipv4']['address']` even though dbservers were not part of the play, or left out by using `--limit`.
|
||||
|
|
|
@ -80,7 +80,7 @@ As of Ansible 1.8, it is possible to use the default filter to omit module param
|
|||
|
||||
- name: touch files with an optional mode
|
||||
file: dest={{item.path}} state=touch mode={{item.mode|default(omit)}}
|
||||
with_items:
|
||||
loop:
|
||||
- path: /tmp/foo
|
||||
- path: /tmp/bar
|
||||
- path: /tmp/baz
|
||||
|
@ -234,7 +234,7 @@ JSON Query Filter
|
|||
|
||||
.. versionadded:: 2.2
|
||||
|
||||
Sometimes you end up with a complex data structure in JSON format and you need to extract only a small set of data within it. The **json_query** filter lets you query a complex JSON structure and iterate over it using a with_items structure.
|
||||
Sometimes you end up with a complex data structure in JSON format and you need to extract only a small set of data within it. The **json_query** filter lets you query a complex JSON structure and iterate over it using a loop structure.
|
||||
|
||||
.. note:: This filter is built upon **jmespath**, and you can use the same syntax. For examples, see `jmespath examples <http://jmespath.org/examples.html>`_.
|
||||
|
||||
|
@ -268,19 +268,19 @@ To extract all clusters from this structure, you can use the following query::
|
|||
|
||||
- name: "Display all cluster names"
|
||||
debug: var=item
|
||||
with_items: "{{domain_definition|json_query('domain.cluster[*].name')}}"
|
||||
loop: "{{domain_definition|json_query('domain.cluster[*].name')}}"
|
||||
|
||||
Same thing for all server names::
|
||||
|
||||
- name: "Display all server names"
|
||||
debug: var=item
|
||||
with_items: "{{domain_definition|json_query('domain.server[*].name')}}"
|
||||
loop: "{{domain_definition|json_query('domain.server[*].name')}}"
|
||||
|
||||
This example shows ports from cluster1::
|
||||
|
||||
- name: "Display all server names from cluster1"
|
||||
debug: var=item
|
||||
with_items: "{{domain_definition|json_query(server_name_cluster1_query)}}"
|
||||
loop: "{{domain_definition|json_query(server_name_cluster1_query)}}"
|
||||
vars:
|
||||
server_name_cluster1_query: "domain.server[?cluster=='cluster1'].port"
|
||||
|
||||
|
@ -291,7 +291,7 @@ Or, alternatively::
|
|||
- name: "Display all server names from cluster1"
|
||||
debug:
|
||||
var: item
|
||||
with_items: "{{domain_definition|json_query('domain.server[?cluster=`cluster1`].port')}}"
|
||||
loop: "{{domain_definition|json_query('domain.server[?cluster=`cluster1`].port')}}"
|
||||
|
||||
.. note:: Here, quoting literals using backticks avoids escaping quotes and maintains readability.
|
||||
|
||||
|
@ -299,7 +299,7 @@ In this example, we get a hash map with all ports and names of a cluster::
|
|||
|
||||
- name: "Display all server ports and names from cluster1"
|
||||
debug: var=item
|
||||
with_items: "{{domain_definition|json_query(server_name_cluster1_query)}}"
|
||||
loop: "{{domain_definition|json_query(server_name_cluster1_query)}}"
|
||||
vars:
|
||||
server_name_cluster1_query: "domain.server[?cluster=='cluster2'].{name: name, port: port}"
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
Lookups
|
||||
-------
|
||||
|
||||
Lookup plugins allow access to outside data sources. Like all templating, these plugins are evaluated on the Ansible control machine, and can include reading the filesystem as well as contacting external datastores and services. This data is then made available using the standard templating system in Ansible.
|
||||
Lookup plugins allow access to outside data sources. Like all templating, these plugins are evaluated on the Ansible control machine, and can include reading the filesystem as well as contacting external datastores and services. This data is then made available using the standard templating system in Ansible.
|
||||
|
||||
.. note::
|
||||
- Lookups occur on the local computer, not on the remote computer.
|
||||
|
@ -13,578 +13,28 @@ Lookup plugins allow access to outside data sources. Like all templating, these
|
|||
|
||||
.. contents:: Topics
|
||||
|
||||
.. _getting_file_contents:
|
||||
.. _lookups_and_loops:
|
||||
|
||||
Intro to Lookups: Getting File Contents
|
||||
```````````````````````````````````````
|
||||
Lookups and loops
|
||||
`````````````````
|
||||
|
||||
The file lookup is the most basic lookup type.
|
||||
Various *lookup plugins* allow additional ways to iterate over data.
|
||||
In :doc:`Loops <playbooks_loops>` you will learn how to use them to walk over collections of numerous types.
|
||||
However, they can also be used to pull in data from remote sources, such as shell commands or even key value stores.
|
||||
Before Ansible 2.5, lookups were mostly used indirectly in ``with_<lookup`` constructes for looping, begining in 2.5
|
||||
we use them more explicitly as part of Jinja2 expressions fed into the ``loop`` keyword.
|
||||
|
||||
Contents can be read from the filesystem as follows::
|
||||
|
||||
---
|
||||
- hosts: all
|
||||
vars:
|
||||
contents: "{{ lookup('file', '/etc/foo.txt') }}"
|
||||
.. _lookups_and_variables:
|
||||
|
||||
tasks:
|
||||
Lookups and variables
|
||||
`````````````````````
|
||||
|
||||
- debug: msg="the value of foo.txt is {{ contents }}"
|
||||
|
||||
.. _password_lookup:
|
||||
|
||||
The Password Lookup
|
||||
```````````````````
|
||||
|
||||
.. note::
|
||||
|
||||
We recommend using :doc:`playbooks_vault` instead of the password lookup plugin if you don't need to generate random passwords on a per-host basis.
|
||||
|
||||
The ``password lookup`` plugin generates a random plaintext password and stores it in
|
||||
a file at a given filepath.
|
||||
|
||||
If the file already exists, it will retrieve its contents, behaving just like with_file. Usage of variables like "{{ inventory_hostname }}" in the filepath can be used to set up random passwords per host (which simplifies password management in '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.
|
||||
|
||||
Generated passwords contain a random mix of upper and lowercase ASCII letters, the
|
||||
numbers 0-9 and punctuation (". , : - _"). The default length of a generated password is 20 characters.
|
||||
This length can be changed by passing an extra parameter::
|
||||
|
||||
---
|
||||
- hosts: all
|
||||
|
||||
tasks:
|
||||
|
||||
- name: create a mysql user with a random password
|
||||
mysql_user:
|
||||
name: "{{ client }}"
|
||||
password: "{{ lookup('password', 'credentials/' + client + '/' + tier + '/' + role + '/mysqlpassword length=15') }}"
|
||||
priv: "{{ client }}_{{ tier }}_{{ role }}.*:ALL"
|
||||
|
||||
# (...)
|
||||
|
||||
.. note:: 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.
|
||||
|
||||
.. warning:: Since the password lookup 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. Attempts to write into areas such as /etc will fail unless the entire playbook is being run as root.
|
||||
|
||||
Starting in Ansible version 1.4, password accepts a "chars" parameter to allow defining a custom character set in the generated passwords. It accepts comma separated list of names that are either string module attributes (such as ascii_letters or digits) or are used literally::
|
||||
|
||||
---
|
||||
- hosts: all
|
||||
|
||||
tasks:
|
||||
|
||||
- name: create a mysql user with a random password using only ascii letters
|
||||
mysql_user: name={{ client }} password="{{ lookup('password', '/tmp/passwordfile chars=ascii_letters') }}" priv={{ client }}_{{ tier }}_{{ role }}.*:ALL
|
||||
|
||||
- name: create a mysql user with a random password using only digits
|
||||
mysql_user:
|
||||
name: "{{ client }}"
|
||||
password: "{{ lookup('password', '/tmp/passwordfile chars=digits') }}"
|
||||
priv: "{{ client }}_{{ tier }}_{{ role }}.*:ALL"
|
||||
|
||||
- name: create a mysql user with a random password using many different char sets
|
||||
mysql_user:
|
||||
name: "{{ client }}"
|
||||
password: "{{ lookup('password', '/tmp/passwordfile chars=ascii_letters,digits,hexdigits,punctuation') }}"
|
||||
priv: "{{ client }}_{{ tier }}_{{ role }}.*:ALL"
|
||||
|
||||
# (...)
|
||||
|
||||
To enter a comma use two commas ',,' somewhere - preferably at the end. Quotes and double quotes are not supported.
|
||||
|
||||
.. _passwordstore_lookup:
|
||||
|
||||
The Passwordstore Lookup
|
||||
````````````````````````
|
||||
.. versionadded:: 2.3
|
||||
|
||||
The ``passwordstore`` lookup enables Ansible to retrieve, create or update passwords from
|
||||
the passwordstore.org_ ``pass`` utility. It also retrieves YAML style keys stored as multilines
|
||||
in the passwordfile.
|
||||
|
||||
.. _passwordstore.org: https://www.passwordstore.org
|
||||
|
||||
Examples
|
||||
--------
|
||||
Basic lookup. Fails if example/test doesn't exist::
|
||||
|
||||
password="{{ lookup('passwordstore', 'example/test')}}"
|
||||
|
||||
Create pass with random 16 character password. If password exists just give the password::
|
||||
|
||||
password="{{ lookup('passwordstore', 'example/test create=true')}}"
|
||||
|
||||
Different size password::
|
||||
|
||||
password="{{ lookup('passwordstore', 'example/test create=true length=42')}}"
|
||||
|
||||
Create password and overwrite the password if it exists. As a bonus, this module includes the old password inside the pass file::
|
||||
|
||||
password="{{ lookup('passwordstore', 'example/test create=true overwrite=true')}}"
|
||||
|
||||
Return the value for user in the KV pair user: username::
|
||||
|
||||
password="{{ lookup('passwordstore', 'example/test subkey=user')}}"
|
||||
|
||||
Return the entire password file content::
|
||||
|
||||
password="{{ lookup('passwordstore', 'example/test returnall=true')}}"
|
||||
|
||||
The location of the password-store directory can be specified in the following ways:
|
||||
- Default is ~/.password-store
|
||||
- Can be overruled by PASSWORD_STORE_DIR environment variable
|
||||
- Can be overruled by 'passwordstore: path/to/.password-store' ansible setting
|
||||
- Can be overruled by 'directory=path' argument in the lookup call
|
||||
|
||||
.. _csvfile_lookup:
|
||||
|
||||
The CSV File Lookup
|
||||
```````````````````
|
||||
.. versionadded:: 1.5
|
||||
|
||||
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``, and
|
||||
returns the value in the second column, unless a different column is specified.
|
||||
|
||||
The example below shows the contents of a CSV file named elements.csv with information about the
|
||||
periodic table of elements::
|
||||
|
||||
Symbol,Atomic Number,Atomic Mass
|
||||
H,1,1.008
|
||||
He,2,4.0026
|
||||
Li,3,6.94
|
||||
Be,4,9.012
|
||||
B,5,10.81
|
||||
|
||||
|
||||
We can use the ``csvfile`` plugin to look up the atomic number or atomic of Lithium by its symbol::
|
||||
|
||||
- debug: msg="The atomic number of Lithium is {{ lookup('csvfile', 'Li file=elements.csv delimiter=,') }}"
|
||||
- debug: msg="The atomic mass of Lithium is {{ lookup('csvfile', 'Li file=elements.csv delimiter=, col=2') }}"
|
||||
|
||||
|
||||
The ``csvfile`` lookup supports several arguments. The format for passing
|
||||
arguments is::
|
||||
|
||||
lookup('csvfile', 'key arg1=val1 arg2=val2 ...')
|
||||
|
||||
The first value in the argument is the ``key``, which must be an entry that
|
||||
appears exactly once in column 0 (the first column, 0-indexed) of the table. All other arguments are optional.
|
||||
|
||||
|
||||
========== ============ =========================================================================================
|
||||
Field Default Description
|
||||
---------- ------------ -----------------------------------------------------------------------------------------
|
||||
file ansible.csv Name of the file to load
|
||||
col 1 The column to output, indexed by 0
|
||||
delimiter TAB Delimiter used by CSV file. As a special case, tab can be specified as either TAB or \t.
|
||||
default empty string Default return value if the key is not in the csv file
|
||||
encoding utf-8 Encoding (character set) of the used CSV file (added in version 2.1)
|
||||
========== ============ =========================================================================================
|
||||
|
||||
.. note:: The default delimiter is TAB, *not* comma.
|
||||
|
||||
.. _ini_lookup:
|
||||
|
||||
The INI File Lookup
|
||||
```````````````````
|
||||
.. versionadded:: 2.0
|
||||
|
||||
The ``ini`` lookup reads the contents of a file in INI format (key1=value1).
|
||||
This plugin retrieve the value on the right side after the equal sign ('=') of
|
||||
a given section ([section]). You can also read a property file which - in this
|
||||
case - does not contain section.
|
||||
|
||||
Here's a simple example of an INI file with user/password configuration:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[production]
|
||||
# My production information
|
||||
user=robert
|
||||
pass=somerandompassword
|
||||
|
||||
[integration]
|
||||
# My integration information
|
||||
user=gertrude
|
||||
pass=anotherpassword
|
||||
|
||||
|
||||
We can use the ``ini`` plugin to lookup user configuration::
|
||||
|
||||
- debug: msg="User in integration is {{ lookup('ini', 'user section=integration file=users.ini') }}"
|
||||
- debug: msg="User in production is {{ lookup('ini', 'user section=production file=users.ini') }}"
|
||||
|
||||
Another example for this plugin is for looking for a value on java properties.
|
||||
Here's a simple properties we'll take as an example:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
user.name=robert
|
||||
user.pass=somerandompassword
|
||||
|
||||
You can retrieve the ``user.name`` field with the following lookup::
|
||||
|
||||
- debug: msg="user.name is {{ lookup('ini', 'user.name type=properties file=user.properties') }}"
|
||||
|
||||
The ``ini`` lookup supports several arguments like the csv plugin. The format for passing
|
||||
arguments is::
|
||||
|
||||
lookup('ini', 'key [type=<properties|ini>] [section=section] [file=file.ini] [re=true] [default=<defaultvalue>]')
|
||||
|
||||
The first value in the argument is the ``key``, which must be an entry that
|
||||
appears exactly once on keys. All other arguments are optional.
|
||||
|
||||
|
||||
========== ============ =========================================================================================
|
||||
Field Default Description
|
||||
---------- ------------ -----------------------------------------------------------------------------------------
|
||||
type ini Type of the file. Can be ini or properties (for java properties).
|
||||
file ansible.ini Name of the file to load
|
||||
section global Default section where to lookup for key.
|
||||
re False The key is a regexp.
|
||||
encoding utf-8 Text encoding to use.
|
||||
default empty string return value if the key is not in the ini file
|
||||
========== ============ =========================================================================================
|
||||
|
||||
.. note:: In java properties files, there's no need to specify a section.
|
||||
|
||||
.. _credstash_lookup:
|
||||
|
||||
The Credstash Lookup
|
||||
````````````````````
|
||||
.. versionadded:: 2.0
|
||||
|
||||
Credstash is a small utility for managing secrets using AWS's KMS and DynamoDB: https://github.com/fugue/credstash
|
||||
|
||||
First, you need to store your secrets with credstash:
|
||||
|
||||
.. code-block:: shell-session
|
||||
|
||||
credstash put my-github-password secure123
|
||||
|
||||
# my-github-password has been stored
|
||||
|
||||
|
||||
Example usage::
|
||||
|
||||
|
||||
---
|
||||
- name: "Test credstash lookup plugin -- get my github password"
|
||||
debug: msg="Credstash lookup! {{ lookup('credstash', 'my-github-password') }}"
|
||||
|
||||
|
||||
You can specify regions or tables to fetch secrets from::
|
||||
|
||||
|
||||
---
|
||||
- name: "Test credstash lookup plugin -- get my other password from us-west-1"
|
||||
debug: msg="Credstash lookup! {{ lookup('credstash', 'my-other-password', region='us-west-1') }}"
|
||||
|
||||
|
||||
- name: "Test credstash lookup plugin -- get the company's github password"
|
||||
debug: msg="Credstash lookup! {{ lookup('credstash', 'company-github-password', table='company-passwords') }}"
|
||||
|
||||
|
||||
If you use the context feature when putting your secret, you can get it by passing a dictionary to the context option like this::
|
||||
|
||||
---
|
||||
- name: test
|
||||
hosts: localhost
|
||||
vars:
|
||||
context:
|
||||
app: my_app
|
||||
environment: production
|
||||
tasks:
|
||||
|
||||
- name: "Test credstash lookup plugin -- get the password with a context passed as a variable"
|
||||
debug: msg="{{ lookup('credstash', 'some-password', context=context) }}"
|
||||
|
||||
- name: "Test credstash lookup plugin -- get the password with a context defined here"
|
||||
debug: msg="{{ lookup('credstash', 'some-password', context=dict(app='my_app', environment='production')) }}"
|
||||
|
||||
If you're not using 2.0 yet, you can do something similar with the credstash tool and the pipe lookup (see below)::
|
||||
|
||||
debug: msg="Poor man's credstash lookup! {{ lookup('pipe', 'credstash -r us-west-1 get my-other-password') }}"
|
||||
|
||||
.. _dns_lookup:
|
||||
|
||||
The DNS Lookup (dig)
|
||||
````````````````````
|
||||
.. versionadded:: 1.9.0
|
||||
|
||||
.. warning:: This lookup depends on the `dnspython <http://www.dnspython.org/>`_
|
||||
library.
|
||||
|
||||
The ``dig`` lookup runs queries against DNS servers to retrieve DNS records for
|
||||
a specific name (*FQDN* - fully qualified domain name). It is possible to lookup any DNS record in this manner.
|
||||
|
||||
There is a couple of different syntaxes that can be used to specify what record
|
||||
should be retrieved, and for which name. It is also possible to explicitly
|
||||
specify the DNS server(s) to use for lookups.
|
||||
|
||||
In its simplest form, the ``dig`` lookup plugin can be used to retrieve an IPv4
|
||||
address (DNS ``A`` record) associated with *FQDN*:
|
||||
|
||||
.. note:: If you need to obtain the ``AAAA`` record (IPv6 address), you must
|
||||
specify the record type explicitly. Syntax for specifying the record
|
||||
type is described below.
|
||||
|
||||
.. note:: The trailing dot in most of the examples listed is purely optional,
|
||||
but is specified for completeness/correctness sake.
|
||||
|
||||
::
|
||||
|
||||
- debug: msg="The IPv4 address for example.com. is {{ lookup('dig', 'example.com.')}}"
|
||||
|
||||
In addition to (default) ``A`` record, it is also possible to specify a different
|
||||
record type that should be queried. This can be done by either passing-in
|
||||
additional parameter of format ``qtype=TYPE`` to the ``dig`` lookup, or by
|
||||
appending ``/TYPE`` to the *FQDN* being queried. For example::
|
||||
|
||||
- debug: msg="The TXT record for example.org. is {{ lookup('dig', 'example.org.', 'qtype=TXT') }}"
|
||||
- debug: msg="The TXT record for example.org. is {{ lookup('dig', 'example.org./TXT') }}"
|
||||
|
||||
If multiple values are associated with the requested record, the results will be
|
||||
returned as a comma-separated list. In such cases you may want to pass option
|
||||
``wantlist=True`` to the plugin, which will result in the record values being
|
||||
returned as a list over which you can iterate later on::
|
||||
|
||||
- debug: msg="One of the MX records for gmail.com. is {{ item }}"
|
||||
with_items: "{{ lookup('dig', 'gmail.com./MX', wantlist=True) }}"
|
||||
|
||||
In case of reverse DNS lookups (``PTR`` records), you can also use a convenience
|
||||
syntax of format ``IP_ADDRESS/PTR``. The following three lines would produce the
|
||||
same output::
|
||||
|
||||
- debug: msg="Reverse DNS for 192.0.2.5 is {{ lookup('dig', '192.0.2.5/PTR') }}"
|
||||
- debug: msg="Reverse DNS for 192.0.2.5 is {{ lookup('dig', '5.2.0.192.in-addr.arpa./PTR') }}"
|
||||
- debug: msg="Reverse DNS for 192.0.2.5 is {{ lookup('dig', '5.2.0.192.in-addr.arpa.', 'qtype=PTR') }}"
|
||||
|
||||
By default, the lookup will rely on system-wide configured DNS servers for
|
||||
performing the query. It is also possible to explicitly specify DNS servers to
|
||||
query using the ``@DNS_SERVER_1,DNS_SERVER_2,...,DNS_SERVER_N`` notation. This
|
||||
needs to be passed-in as an additional parameter to the lookup. For example::
|
||||
|
||||
- debug: msg="Querying 198.51.100.23 for IPv4 address for example.com. produces {{ lookup('dig', 'example.com', '@198.51.100.23') }}"
|
||||
|
||||
In some cases the DNS records may hold a more complex data structure, or it may
|
||||
be useful to obtain the results in a form of a dictionary for future
|
||||
processing. The ``dig`` lookup supports parsing of a number of such records,
|
||||
with the result being returned as a dictionary. This way it is possible to
|
||||
easily access such nested data. This return format can be requested by
|
||||
passing-in the ``flat=0`` option to the lookup. For example::
|
||||
|
||||
- debug: msg="XMPP service for gmail.com. is available at {{ item.target }} on port {{ item.port }}"
|
||||
with_items: "{{ lookup('dig', '_xmpp-server._tcp.gmail.com./SRV', 'flat=0', wantlist=True) }}"
|
||||
|
||||
Take note that due to the way Ansible lookups work, you must pass the
|
||||
``wantlist=True`` argument to the lookup, otherwise Ansible will report errors.
|
||||
|
||||
Currently the dictionary results are supported for the following records:
|
||||
|
||||
.. note:: *ALL* is not a record per-se, merely the listed fields are available
|
||||
for any record results you retrieve in the form of a dictionary.
|
||||
|
||||
========== =============================================================================
|
||||
Record Fields
|
||||
---------- -----------------------------------------------------------------------------
|
||||
*ALL* owner, ttl, type
|
||||
A address
|
||||
AAAA address
|
||||
CNAME target
|
||||
DNAME target
|
||||
DLV algorithm, digest_type, key_tag, digest
|
||||
DNSKEY flags, algorithm, protocol, key
|
||||
DS algorithm, digest_type, key_tag, digest
|
||||
HINFO cpu, os
|
||||
LOC latitude, longitude, altitude, size, horizontal_precision, vertical_precision
|
||||
MX preference, exchange
|
||||
NAPTR order, preference, flags, service, regexp, replacement
|
||||
NS target
|
||||
NSEC3PARAM algorithm, flags, iterations, salt
|
||||
PTR target
|
||||
RP mbox, txt
|
||||
SOA mname, rname, serial, refresh, retry, expire, minimum
|
||||
SPF strings
|
||||
SRV priority, weight, port, target
|
||||
SSHFP algorithm, fp_type, fingerprint
|
||||
TLSA usage, selector, mtype, cert
|
||||
TXT strings
|
||||
========== =============================================================================
|
||||
|
||||
.. _mongodb_lookup:
|
||||
|
||||
MongoDB Lookup
|
||||
``````````````
|
||||
.. versionadded:: 2.3
|
||||
|
||||
.. warning:: This lookup depends on the `pymongo 2.4+ <http://www.mongodb.org/>`_
|
||||
library.
|
||||
|
||||
|
||||
The ``MongoDB`` lookup runs the *find()* command on a given *collection* on a given *MongoDB* server.
|
||||
|
||||
The result is a list of jsons, so slightly different from what PyMongo returns. In particular, *timestamps* are converted to epoch integers.
|
||||
|
||||
Currently, the following parameters are supported.
|
||||
|
||||
=========================== ========= ======= ==================== =======================================================================================================================================================================
|
||||
Parameter Mandatory Type Default Value Comment
|
||||
--------------------------- --------- ------- -------------------- -----------------------------------------------------------------------------------------------------------------------------------------------------------------------
|
||||
connection_string no string mongodb://localhost/ Can be any valid MongoDB connection string, supporting authentication, replicasets, etc. More info at https://docs.mongodb.org/manual/reference/connection-string/
|
||||
extra_connection_parameters no dict {} Dictionary with extra parameters like ssl, ssl_keyfile, maxPoolSize etc... Check the full list here: https://api.mongodb.org/python/current/api/pymongo/mongo_client.html#pymongo.mongo_client.MongoClient
|
||||
database yes string Name of the database which the query will be made
|
||||
collection yes string Name of the collection which the query will be made
|
||||
filter no dict [pymongo default] Criteria of the output Example: { "hostname": "batman" }
|
||||
projection no dict [pymongo default] Fields you want returned. Example: { "pid": True , "_id" : False , "hostname" : True }
|
||||
skip no integer [pymongo default] How many results should be skept
|
||||
limit no integer [pymongo default] How many results should be shown
|
||||
sort no list [pymongo default] Sorting rules. Please notice the constats are replaced by strings. [ [ "startTime" , "ASCENDING" ] , [ "age", "DESCENDING" ] ]
|
||||
[any find() parameter] no [any] [pymongo default] Every parameter with exception to *connection_string*, *database* and *collection* are passed to pymongo directly.
|
||||
=========================== ========= ======= ==================== =======================================================================================================================================================================
|
||||
|
||||
Please check https://api.mongodb.org/python/current/api/pymongo/collection.html?highlight=find#pymongo.collection.Collection.find for more detais.
|
||||
|
||||
Since there are too many parameters for this lookup method, below is a sample playbook which shows its usage and a nice way to feed the parameters:
|
||||
|
||||
.. code-block:: yaml
|
||||
|
||||
---
|
||||
- hosts: all
|
||||
gather_facts: false
|
||||
|
||||
vars:
|
||||
mongodb_parameters:
|
||||
#optional parameter, default = "mongodb://localhost/"
|
||||
# connection_string: "mongodb://localhost/"
|
||||
# extra_connection_parameters: { "ssl" : True , "ssl_certfile": /etc/self_signed_certificate.pem" }
|
||||
|
||||
#mandatory parameters
|
||||
database: 'local'
|
||||
collection: "startup_log"
|
||||
|
||||
#optional query parameters
|
||||
#we accept any parameter from the normal mongodb query.
|
||||
# the official documentation is here
|
||||
# https://api.mongodb.org/python/current/api/pymongo/collection.html?highlight=find#pymongo.collection.Collection.find
|
||||
# filter: { "hostname": "batman" }
|
||||
projection: { "pid": True , "_id" : False , "hostname" : True }
|
||||
# skip: 0
|
||||
limit: 1
|
||||
# sort: [ [ "startTime" , "ASCENDING" ] , [ "age", "DESCENDING" ] ]
|
||||
|
||||
tasks:
|
||||
- debug: msg="Mongo has already started with the following PID [{{ item.pid }}]"
|
||||
with_mongodb: "{{mongodb_parameters}}"
|
||||
|
||||
|
||||
|
||||
Sample output:
|
||||
|
||||
.. code-block:: shell-session
|
||||
|
||||
mdiez@batman:~/ansible$ ansible-playbook m.yml -i localhost.ini
|
||||
|
||||
PLAY [all] *********************************************************************
|
||||
|
||||
TASK [debug] *******************************************************************
|
||||
Sunday 20 March 2016 22:40:39 +0200 (0:00:00.023) 0:00:00.023 **********
|
||||
ok: [localhost] => (item={u'hostname': u'batman', u'pid': 60639L}) => {
|
||||
"item": {
|
||||
"hostname": "batman",
|
||||
"pid": 60639
|
||||
},
|
||||
"msg": "Mongo has already started with the following PID [60639]"
|
||||
}
|
||||
|
||||
PLAY RECAP *********************************************************************
|
||||
localhost : ok=1 changed=0 unreachable=0 failed=0
|
||||
|
||||
Sunday 20 March 2016 22:40:39 +0200 (0:00:00.067) 0:00:00.091 **********
|
||||
===============================================================================
|
||||
debug ------------------------------------------------------------------- 0.07s
|
||||
mdiez@batman:~/ansible$
|
||||
|
||||
|
||||
.. _more_lookups:
|
||||
|
||||
More Lookups
|
||||
````````````
|
||||
|
||||
Various *lookup plugins* allow additional ways to iterate over data. In :doc:`Loops <playbooks_loops>` you will learn
|
||||
how to use them to walk over collections of numerous types. However, they can also be used to pull in data
|
||||
from remote sources, such as shell commands or even key value stores. This section will cover lookup plugins in this capacity.
|
||||
|
||||
Here are some examples::
|
||||
|
||||
---
|
||||
- hosts: all
|
||||
|
||||
tasks:
|
||||
|
||||
- debug: msg="{{ lookup('env','HOME') }} is an environment variable"
|
||||
|
||||
- name: lines will iterate over each line from stdout of a command
|
||||
debug: msg="{{ item }} is a line from the result of this command"
|
||||
with_lines: cat /etc/motd
|
||||
|
||||
- debug: msg="{{ lookup('pipe','date') }} is the raw result of running this command"
|
||||
|
||||
- name: Always use quote filter to make sure your variables are safe to use with shell
|
||||
debug: msg="{{ lookup('pipe','getent ' + myuser|quote ) }}"
|
||||
|
||||
- name: Quote variables with_lines also as it executes shell
|
||||
debug: msg="{{ item }} is a line from myfile"
|
||||
with_lines: "cat {{myfile|quote}}"
|
||||
|
||||
- name: redis_kv lookup requires the Python redis package
|
||||
debug: msg="{{ lookup('redis_kv', 'redis://localhost:6379,somekey') }} is value in Redis for somekey"
|
||||
|
||||
- name: dnstxt lookup requires the Python dnspython package
|
||||
debug: msg="{{ lookup('dnstxt', 'example.com') }} is a DNS TXT record for example.com"
|
||||
|
||||
- debug: msg="{{ lookup('template', './some_template.j2') }} is a value from evaluation of this template"
|
||||
|
||||
# Since 2.4, you can pass in variables during evaluation
|
||||
- debug: msg="{{ lookup('template', './some_template.j2', template_vars=dict(x=42)) }} is evaluated with x=42"
|
||||
|
||||
- name: loading a json file from a template as a string
|
||||
debug: msg="{{ lookup('template', './some_json.json.j2', convert_data=False) }} is a value from evaluation of this template"
|
||||
|
||||
|
||||
- debug: msg="{{ lookup('etcd', 'foo') }} is a value from a locally running etcd"
|
||||
|
||||
# shelvefile lookup retrieves a string value corresponding to a key inside a Python shelve file
|
||||
- debug: msg="{{ lookup('shelvefile', 'file=path_to_some_shelve_file.db key=key_to_retrieve') }}
|
||||
|
||||
# The following lookups were added in 1.9
|
||||
# url lookup splits lines by default, an option to disable this was added in 2.4
|
||||
- debug: msg="{{item}}"
|
||||
with_url:
|
||||
- 'https://github.com/gremlin.keys'
|
||||
|
||||
# outputs the cartesian product of the supplied lists
|
||||
- debug: msg="{{item}}"
|
||||
with_cartesian:
|
||||
- "{{list1}}"
|
||||
- "{{list2}}"
|
||||
- [1,2,3,4,5,6]
|
||||
|
||||
- name: Added in 2.3 allows using the system's keyring
|
||||
debug: msg={{lookup('keyring','myservice myuser')}}
|
||||
|
||||
|
||||
As an alternative, you can also assign lookup plugins to variables or use them elsewhere.
|
||||
These macros are evaluated each time they are used in a task (or template)::
|
||||
One way of using lookups is to populate variables. These macros are evaluated each time they are used in a task (or template)::
|
||||
|
||||
vars:
|
||||
motd_value: "{{ lookup('file', '/etc/motd') }}"
|
||||
|
||||
tasks:
|
||||
|
||||
- debug: msg="motd value is {{ motd_value }}"
|
||||
|
||||
.. seealso::
|
||||
|
|
|
@ -20,13 +20,13 @@ To save some typing, repeated tasks can be written in short-hand like so::
|
|||
name: "{{ item }}"
|
||||
state: present
|
||||
groups: "wheel"
|
||||
with_items:
|
||||
loop:
|
||||
- testuser1
|
||||
- testuser2
|
||||
|
||||
If you have defined a YAML list in a variables file, or the 'vars' section, you can also do::
|
||||
|
||||
with_items: "{{ somelist }}"
|
||||
loop: "{{ somelist }}"
|
||||
|
||||
The above would be the equivalent of::
|
||||
|
||||
|
@ -41,9 +41,20 @@ The above would be the equivalent of::
|
|||
state: present
|
||||
groups: "wheel"
|
||||
|
||||
The yum and apt modules use with_items to execute fewer package manager transactions.
|
||||
.. note:: Before 2.5 Ansible mainly used the `with_<lookup>` keywords to create loops, the `loop` keyword is basically analogous to `with_list`.
|
||||
|
||||
Note that the types of items you iterate over with 'with_items' do not have to be simple lists of strings.
|
||||
|
||||
Some plugins like, the yum and apt modules can take lists directly to their options, this is more optimal than looping over the task.
|
||||
See each action's documentation for details, for now here is an example::
|
||||
|
||||
- name: optimal yum
|
||||
yum: name={{list_of_packages}} state=present
|
||||
|
||||
- name: non optimal yum, not only slower but might cause issues with interdependencies
|
||||
yum: name={{item}} state=present
|
||||
loop: "{{list_of_packages}}"
|
||||
|
||||
Note that the types of items you iterate over do not have to be simple lists of strings.
|
||||
If you have a list of hashes, you can reference subkeys using things like::
|
||||
|
||||
- name: add several users
|
||||
|
@ -51,45 +62,21 @@ If you have a list of hashes, you can reference subkeys using things like::
|
|||
name: "{{ item.name }}"
|
||||
state: present
|
||||
groups: "{{ item.groups }}"
|
||||
with_items:
|
||||
loop:
|
||||
- { name: 'testuser1', groups: 'wheel' }
|
||||
- { name: 'testuser2', groups: 'root' }
|
||||
|
||||
Also be aware that when combining `when` with `with_items` (or any other loop statement), the `when` statement is processed separately for each item. See :ref:`the_when_statement` for an example.
|
||||
Also be aware that when combining :ref:`when: playbooks_conditionals` with a loop, the ``when:`` statement is processed separately for each item.
|
||||
See :ref:`the_when_statement` for an example.
|
||||
|
||||
Loops are actually a combination of things `with_` + `lookup()`, so any lookup plugin can be used as a source for a loop, 'items' is lookup.
|
||||
|
||||
Please note that ``with_items`` flattens the first depth of the list it is
|
||||
provided and can yield unexpected results if you pass a list which is composed
|
||||
of lists. You can work around this by wrapping your nested list inside a list::
|
||||
.. _complex_loops:
|
||||
|
||||
# This will run debug three times since the list is flattened
|
||||
- debug:
|
||||
msg: "{{ item }}"
|
||||
vars:
|
||||
nested_list:
|
||||
- - one
|
||||
- two
|
||||
- three
|
||||
with_items: "{{ nested_list }}"
|
||||
Complex loops
|
||||
`````````````
|
||||
|
||||
# This will run debug once with the three items
|
||||
- debug:
|
||||
msg: "{{ item }}"
|
||||
vars:
|
||||
nested_list:
|
||||
- - one
|
||||
- two
|
||||
- three
|
||||
with_items:
|
||||
- "{{ nested_list }}"
|
||||
|
||||
.. _nested_loops:
|
||||
|
||||
Nested Loops
|
||||
````````````
|
||||
|
||||
Loops can be nested as well::
|
||||
Sometimes you need more than what a simple list provides, you can use Jinja2 expressions to create complex lists:
|
||||
For example, using the 'nested' lookup, you can combine lists::
|
||||
|
||||
- name: give users access to multiple databases
|
||||
mysql_user:
|
||||
|
@ -97,336 +84,13 @@ Loops can be nested as well::
|
|||
priv: "{{ item[1] }}.*:ALL"
|
||||
append_privs: yes
|
||||
password: "foo"
|
||||
with_nested:
|
||||
- [ 'alice', 'bob' ]
|
||||
- [ 'clientdb', 'employeedb', 'providerdb' ]
|
||||
loop: "{{ lookup('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
|
||||
mysql_user:
|
||||
name: "{{ item[0] }}"
|
||||
priv: "{{ item[1] }}.*:ALL"
|
||||
append_privs: yes
|
||||
password: "foo"
|
||||
with_nested:
|
||||
- "{{ users }}"
|
||||
- [ 'clientdb', 'employeedb', 'providerdb' ]
|
||||
:doc:`Jinja2 lookups playbooks_lookups`, :doc:`filters playbooks_filters` and :doc:`tests playbooks_tests`
|
||||
make for some powerful data generation and manipulation.
|
||||
|
||||
.. _looping_over_hashes:
|
||||
|
||||
Looping over Hashes
|
||||
```````````````````
|
||||
|
||||
.. versionadded:: 1.5
|
||||
|
||||
Suppose you have the following variable::
|
||||
|
||||
---
|
||||
users:
|
||||
alice:
|
||||
name: Alice Appleworth
|
||||
telephone: 123-456-7890
|
||||
bob:
|
||||
name: Bob Bananarama
|
||||
telephone: 987-654-3210
|
||||
|
||||
And you want to print every user's name and phone number. You can loop through the elements of a hash using ``with_dict`` like this::
|
||||
|
||||
tasks:
|
||||
- name: Print phone records
|
||||
debug:
|
||||
msg: "User {{ item.key }} is {{ item.value.name }} ({{ item.value.telephone }})"
|
||||
with_dict: "{{ users }}"
|
||||
|
||||
.. _looping_over_fileglobs:
|
||||
|
||||
Looping over Files
|
||||
``````````````````
|
||||
|
||||
``with_file`` iterates over the content of a list of files, `item` will be set to the content of each file in sequence. It can be used like this::
|
||||
|
||||
---
|
||||
- hosts: all
|
||||
|
||||
tasks:
|
||||
|
||||
# emit a debug message containing the content of each file.
|
||||
- debug:
|
||||
msg: "{{ item }}"
|
||||
with_file:
|
||||
- first_example_file
|
||||
- second_example_file
|
||||
|
||||
Assuming that ``first_example_file`` contained the text "hello" and ``second_example_file`` contained the text "world", this would result in:
|
||||
|
||||
.. code-block:: shell-session
|
||||
|
||||
TASK [debug msg={{ item }}] ******************************************************
|
||||
ok: [localhost] => (item=hello) => {
|
||||
"item": "hello",
|
||||
"msg": "hello"
|
||||
}
|
||||
ok: [localhost] => (item=world) => {
|
||||
"item": "world",
|
||||
"msg": "world"
|
||||
}
|
||||
|
||||
Looping over Fileglobs
|
||||
``````````````````````
|
||||
|
||||
``with_fileglob`` matches all files in a single directory, non-recursively, that match a pattern. It calls
|
||||
`Python's glob library <https://docs.python.org/2/library/glob.html>`_, and can be used like this::
|
||||
|
||||
---
|
||||
- hosts: all
|
||||
|
||||
tasks:
|
||||
|
||||
# first ensure our target directory exists
|
||||
- name: Ensure target directory exists
|
||||
file:
|
||||
dest: "/etc/fooapp"
|
||||
state: directory
|
||||
|
||||
# copy each file over that matches the given pattern
|
||||
- name: Copy each file over that matches the given pattern
|
||||
copy:
|
||||
src: "{{ item }}"
|
||||
dest: "/etc/fooapp/"
|
||||
owner: "root"
|
||||
mode: 0600
|
||||
with_fileglob:
|
||||
- "/playbooks/files/fooapp/*"
|
||||
|
||||
.. note:: When using a relative path with ``with_fileglob`` in a role, Ansible resolves the path relative to the `roles/<rolename>/files` directory.
|
||||
|
||||
|
||||
Looping over Filetrees
|
||||
``````````````````````
|
||||
|
||||
``with_filetree`` recursively matches all files in a directory tree, enabling you to template a complete tree of files on a target system while retaining permissions and ownership.
|
||||
|
||||
The ``filetree`` lookup-plugin supports directories, files and symlinks, including SELinux and other file properties. Here is a complete list of what each file object consists of:
|
||||
|
||||
* src
|
||||
* root
|
||||
* path
|
||||
* mode
|
||||
* state
|
||||
* owner
|
||||
* group
|
||||
* seuser
|
||||
* serole
|
||||
* setype
|
||||
* selevel
|
||||
* uid
|
||||
* gid
|
||||
* size
|
||||
* mtime
|
||||
* ctime
|
||||
|
||||
If you provide more than one path, it will implement a ``with_first_found`` logic, and will not process entries it already processed in previous paths. This enables the user to merge different trees in order of importance, or add role_vars specific paths to influence different instances of the same role.
|
||||
|
||||
Here is an example of how we use with_filetree within a role. The ``web/`` path is relative to either ``roles/<role>/files/`` or ``files/``::
|
||||
|
||||
---
|
||||
- name: Create directories
|
||||
file:
|
||||
path: /web/{{ item.path }}
|
||||
state: directory
|
||||
mode: '{{ item.mode }}'
|
||||
with_filetree: web/
|
||||
when: item.state == 'directory'
|
||||
|
||||
- name: Template files
|
||||
template:
|
||||
src: '{{ item.src }}'
|
||||
dest: /web/{{ item.path }}
|
||||
mode: '{{ item.mode }}'
|
||||
with_filetree: web/
|
||||
when: item.state == 'file'
|
||||
|
||||
- name: Recreate symlinks
|
||||
file:
|
||||
src: '{{ item.src }}'
|
||||
dest: /web/{{ item.path }}
|
||||
state: link
|
||||
force: yes
|
||||
mode: '{{ item.mode }}'
|
||||
with_filetree: web/
|
||||
when: item.state == 'link'
|
||||
|
||||
|
||||
The following properties are also available:
|
||||
|
||||
* ``root``: allows filtering by original location
|
||||
* ``path``: contains the relative path to root
|
||||
* ``uidi``, ``gid``: force-create by exact id, rather than by name
|
||||
* ``size``, ``mtime``, ``ctime``: filter out files by size, mtime or ctime
|
||||
|
||||
|
||||
Looping over Parallel Sets of Data
|
||||
``````````````````````````````````
|
||||
|
||||
Suppose you have the following variable data::
|
||||
|
||||
---
|
||||
alpha: [ 'a', 'b', 'c', 'd' ]
|
||||
numbers: [ 1, 2, 3, 4 ]
|
||||
|
||||
...and you want the set of '(a, 1)' and '(b, 2)'. Use 'with_together' to get this::
|
||||
|
||||
tasks:
|
||||
- debug:
|
||||
msg: "{{ item.0 }} and {{ item.1 }}"
|
||||
with_together:
|
||||
- "{{ alpha }}"
|
||||
- "{{ numbers }}"
|
||||
|
||||
Looping over Subelements
|
||||
````````````````````````
|
||||
|
||||
Suppose you want to do something like loop over a list of users, creating them, and allowing them to login by a certain set of
|
||||
SSH keys.
|
||||
|
||||
In this example, we'll assume you have the following defined and loaded in via "vars_files" or maybe a "group_vars/all" file::
|
||||
|
||||
---
|
||||
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"
|
||||
- name: bob
|
||||
authorized:
|
||||
- /tmp/bob/id_rsa.pub
|
||||
mysql:
|
||||
password: other-mysql-password
|
||||
hosts:
|
||||
- "db1"
|
||||
privs:
|
||||
- "*.*:SELECT"
|
||||
- "DB2.*:ALL"
|
||||
|
||||
You could loop over these subelements like this::
|
||||
|
||||
- name: Create User
|
||||
user:
|
||||
name: "{{ item.name }}"
|
||||
state: present
|
||||
generate_ssh_key: yes
|
||||
with_items:
|
||||
- "{{ users }}"
|
||||
|
||||
- name: Set authorized ssh key
|
||||
authorized_key:
|
||||
user: "{{ item.0.name }}"
|
||||
key: "{{ lookup('file', item.1) }}"
|
||||
with_subelements:
|
||||
- "{{ users }}"
|
||||
- authorized
|
||||
|
||||
Given the mysql hosts and privs subkey lists, you can also iterate over a list in a nested subkey::
|
||||
|
||||
- name: Setup MySQL users
|
||||
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
|
||||
|
||||
Subelements walks a list of hashes (aka dictionaries) and then traverses a list with a given (nested sub-)key inside of those
|
||||
records.
|
||||
|
||||
Optionally, you can add a third element to the subelements list, that holds a
|
||||
dictionary of flags. Currently you can add the 'skip_missing' flag. If set to
|
||||
True, the lookup plugin will skip the lists items that do not contain the given
|
||||
subkey. Without this flag, or if that flag is set to False, the plugin will
|
||||
yield an error and complain about the missing subkey.
|
||||
|
||||
The authorized_key pattern is exactly where it comes up most.
|
||||
|
||||
.. _looping_over_integer_sequences:
|
||||
|
||||
Looping over Integer Sequences
|
||||
``````````````````````````````
|
||||
|
||||
``with_sequence`` 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 should be specified as key=value pair strings.
|
||||
|
||||
A simple 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).
|
||||
Negative numbers are not supported. This works as follows::
|
||||
|
||||
---
|
||||
- hosts: all
|
||||
|
||||
tasks:
|
||||
|
||||
# create groups
|
||||
- group:
|
||||
name: "evens"
|
||||
state: present
|
||||
- group:
|
||||
name: "odds"
|
||||
state: present
|
||||
|
||||
# create some test users
|
||||
- user:
|
||||
name: "{{ item }}"
|
||||
state: present
|
||||
groups: "evens"
|
||||
with_sequence: start=0 end=32 format=testuser%02x
|
||||
|
||||
# create a series of directories with even numbers for some reason
|
||||
- file:
|
||||
dest: "/var/stuff/{{ item }}"
|
||||
state: directory
|
||||
with_sequence: start=4 end=16 stride=2
|
||||
|
||||
# a simpler way to use the sequence plugin
|
||||
# create 4 groups
|
||||
- group:
|
||||
name: "group{{ item }}"
|
||||
state: present
|
||||
with_sequence: count=4
|
||||
|
||||
.. _playbooks_loops_random_choice:
|
||||
|
||||
Random Choices
|
||||
``````````````
|
||||
|
||||
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::
|
||||
|
||||
- debug:
|
||||
msg: "{{ item }}"
|
||||
with_random_choice:
|
||||
- "go through the door"
|
||||
- "drink from the goblet"
|
||||
- "press the red button"
|
||||
- "do nothing"
|
||||
|
||||
One of the provided strings will be selected at random.
|
||||
|
||||
At a more basic level, they can be used to add chaos and excitement to otherwise predictable automation environments.
|
||||
.. note:: `with_` loops are actually a combination of things `with_` + `lookup()`, even 'items' is a lookup. `loop` can be used in the same way as shown above.
|
||||
|
||||
.. _do_until_loops:
|
||||
|
||||
|
@ -451,179 +115,6 @@ The registered variable will also have a new key "attempts" which will have the
|
|||
|
||||
.. note:: If the "until" parameter isn't defined, the value for the "retries" parameter is forced to 1.
|
||||
|
||||
.. _with_first_found:
|
||||
|
||||
Finding First Matched Files
|
||||
```````````````````````````
|
||||
|
||||
.. note:: This is an uncommon thing to want to do, but we're documenting it for completeness. You probably won't be reaching for this one often.
|
||||
|
||||
This isn't exactly a loop, but it's close. What if you want to use a reference to a file based on the first file found
|
||||
that matches a given criteria, and some of the filenames are determined by variable names? Yes, you can do that as follows::
|
||||
|
||||
- name: INTERFACES | Create Ansible header for /etc/network/interfaces
|
||||
template:
|
||||
src: "{{ item }}"
|
||||
dest: "/etc/foo.conf"
|
||||
with_first_found:
|
||||
- "{{ ansible_virtualization_type }}_foo.conf"
|
||||
- "default_foo.conf"
|
||||
|
||||
This tool also has a long form version that allows for configurable search paths. Here's an example::
|
||||
|
||||
- name: some configuration template
|
||||
template:
|
||||
src: "{{ item }}"
|
||||
dest: "/etc/file.cfg"
|
||||
mode: 0444
|
||||
owner: "root"
|
||||
group: "root"
|
||||
with_first_found:
|
||||
- files:
|
||||
- "{{ inventory_hostname }}/etc/file.cfg"
|
||||
paths:
|
||||
- ../../../templates.overwrites
|
||||
- ../../../templates
|
||||
- files:
|
||||
- etc/file.cfg
|
||||
paths:
|
||||
- templates
|
||||
|
||||
.. _looping_over_the_results_of_a_program_execution:
|
||||
|
||||
Iterating Over The Results of a Program Execution
|
||||
`````````````````````````````````````````````````
|
||||
|
||||
.. note:: This is an uncommon thing to want to do, but we're documenting it for completeness. You probably won't be reaching for this one often.
|
||||
|
||||
Sometimes you might want to execute a program, and based on the output of that program, loop over the results of that line by line.
|
||||
Ansible provides a neat way to do that, though you should remember, this is always executed on the control machine, not the remote
|
||||
machine::
|
||||
|
||||
- name: Example of looping over a command result
|
||||
shell: "/usr/bin/frobnicate {{ item }}"
|
||||
with_lines:
|
||||
- "/usr/bin/frobnications_per_host --param {{ inventory_hostname }}"
|
||||
|
||||
Ok, that was a bit arbitrary. In fact, if you're doing something that is inventory related you might just want to write a dynamic
|
||||
inventory source instead (see :doc:`intro_dynamic_inventory`), but this can be occasionally useful in quick-and-dirty implementations.
|
||||
|
||||
Should you ever need to execute a command remotely, you would not use the above method. Instead do this::
|
||||
|
||||
- name: Example of looping over a REMOTE command result
|
||||
shell: "/usr/bin/something"
|
||||
register: command_result
|
||||
|
||||
- name: Do something with each result
|
||||
shell: "/usr/bin/something_else --param {{ item }}"
|
||||
with_items:
|
||||
- "{{ command_result.stdout_lines }}"
|
||||
|
||||
.. _indexed_lists:
|
||||
|
||||
Looping Over A List With An Index
|
||||
`````````````````````````````````
|
||||
|
||||
.. note:: This is an uncommon thing to want to do, but we're documenting it for completeness. You probably won't be reaching for this one often.
|
||||
|
||||
.. versionadded:: 1.3
|
||||
|
||||
If you want to loop over an array and also get the numeric index of where you are in the array as you go, you can also do that.
|
||||
It's uncommonly used::
|
||||
|
||||
- name: indexed loop demo
|
||||
debug:
|
||||
msg: "at array position {{ item.0 }} there is a value {{ item.1 }}"
|
||||
with_indexed_items:
|
||||
- "{{ some_list }}"
|
||||
|
||||
.. _using_ini_with_a_loop:
|
||||
|
||||
Using ini file with a loop
|
||||
``````````````````````````
|
||||
.. versionadded:: 2.0
|
||||
|
||||
The ini plugin can use regexp to retrieve a set of keys. As a consequence, we can loop over this set. Here is the ini file we'll use:
|
||||
|
||||
.. code-block:: ini
|
||||
|
||||
[section1]
|
||||
value1=section1/value1
|
||||
value2=section1/value2
|
||||
|
||||
[section2]
|
||||
value1=section2/value1
|
||||
value2=section2/value2
|
||||
|
||||
Here is an example of using ``with_ini``::
|
||||
|
||||
- debug:
|
||||
msg: "{{ item }}"
|
||||
with_ini:
|
||||
- value[1-2]
|
||||
- section: section1
|
||||
- file: "lookup.ini"
|
||||
- re: true
|
||||
|
||||
And here is the returned value::
|
||||
|
||||
{
|
||||
"changed": false,
|
||||
"msg": "All items completed",
|
||||
"results": [
|
||||
{
|
||||
"invocation": {
|
||||
"module_args": "msg=\"section1/value1\"",
|
||||
"module_name": "debug"
|
||||
},
|
||||
"item": "section1/value1",
|
||||
"msg": "section1/value1",
|
||||
"verbose_always": true
|
||||
},
|
||||
{
|
||||
"invocation": {
|
||||
"module_args": "msg=\"section1/value2\"",
|
||||
"module_name": "debug"
|
||||
},
|
||||
"item": "section1/value2",
|
||||
"msg": "section1/value2",
|
||||
"verbose_always": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
.. _flattening_a_list:
|
||||
|
||||
Flattening A List
|
||||
`````````````````
|
||||
|
||||
.. note:: This is an uncommon thing to want to do, but we're documenting it for completeness. You probably won't be reaching for this one often.
|
||||
|
||||
In rare instances you might have several lists of lists, and you just want to iterate over every item in all of those lists. Assume
|
||||
a really crazy hypothetical datastructure::
|
||||
|
||||
----
|
||||
# file: roles/foo/vars/main.yml
|
||||
packages_base:
|
||||
- [ 'foo-package', 'bar-package' ]
|
||||
packages_apps:
|
||||
- [ ['one-package', 'two-package' ]]
|
||||
- [ ['red-package'], ['blue-package']]
|
||||
|
||||
As you can see the formatting of packages in these lists is all over the place. How can we install all of the packages in both lists?::
|
||||
|
||||
- name: flattened loop demo
|
||||
yum:
|
||||
name: "{{ item }}"
|
||||
state: present
|
||||
with_flattened:
|
||||
- "{{ packages_base }}"
|
||||
- "{{ packages_apps }}"
|
||||
|
||||
That's how!
|
||||
|
||||
.. _using_register_with_a_loop:
|
||||
|
||||
Using register with a loop
|
||||
``````````````````````````
|
||||
|
||||
|
@ -632,7 +123,7 @@ After using ``register`` with a loop, the data structure placed in the variable
|
|||
Here is an example of using ``register`` with ``with_items``::
|
||||
|
||||
- shell: "echo {{ item }}"
|
||||
with_items:
|
||||
loop:
|
||||
- "one"
|
||||
- "two"
|
||||
register: echo
|
||||
|
@ -682,12 +173,12 @@ Subsequent loops over the registered variable to inspect the results may look li
|
|||
fail:
|
||||
msg: "The command ({{ item.cmd }}) did not have a 0 return code"
|
||||
when: item.rc != 0
|
||||
with_items: "{{ echo.results }}"
|
||||
loop: "{{ echo.results }}"
|
||||
|
||||
During iteration, the result of the current item will be placed in the variable::
|
||||
|
||||
- shell: echo "{{ item }}"
|
||||
with_items:
|
||||
loop:
|
||||
- one
|
||||
- two
|
||||
register: echo
|
||||
|
@ -695,24 +186,22 @@ During iteration, the result of the current item will be placed in the variable:
|
|||
|
||||
|
||||
|
||||
.. _looping_over_the_inventory:
|
||||
|
||||
Looping over the inventory
|
||||
``````````````````````````
|
||||
|
||||
If you wish to loop over the inventory, or just a subset of it, there is multiple ways.
|
||||
One can use a regular ``with_items`` with the ``ansible_play_batch`` or ``groups`` variables, like this::
|
||||
One can use a regular ``loop`` with the ``ansible_play_batch`` or ``groups`` variables, like this::
|
||||
|
||||
# show all the hosts in the inventory
|
||||
- debug:
|
||||
msg: "{{ item }}"
|
||||
with_items:
|
||||
loop:
|
||||
- "{{ groups['all'] }}"
|
||||
|
||||
# show all the hosts in the current play
|
||||
- debug:
|
||||
msg: "{{ item }}"
|
||||
with_items:
|
||||
loop:
|
||||
- "{{ ansible_play_batch }}"
|
||||
|
||||
There is also a specific lookup plugin ``inventory_hostnames`` that can be used like this::
|
||||
|
@ -720,14 +209,12 @@ There is also a specific lookup plugin ``inventory_hostnames`` that can be used
|
|||
# show all the hosts in the inventory
|
||||
- debug:
|
||||
msg: "{{ item }}"
|
||||
with_inventory_hostnames:
|
||||
- all
|
||||
loop: "{{lookup('inventory_hostnames', 'all'}}"
|
||||
|
||||
# show all the hosts matching the pattern, ie all but the group www
|
||||
- debug:
|
||||
msg: "{{ item }}"
|
||||
with_inventory_hostnames:
|
||||
- all:!www
|
||||
loop: "{{lookup('inventory_hostnames', 'all!www'}}"
|
||||
|
||||
More information on the patterns can be found on :doc:`intro_patterns`
|
||||
|
||||
|
@ -738,13 +225,14 @@ Loop Control
|
|||
|
||||
.. versionadded:: 2.1
|
||||
|
||||
In 2.0 you are again able to use `with_` loops and task includes (but not playbook includes). This adds the ability to loop over the set of tasks in one shot.
|
||||
In 2.0 you are again able to use loops and task includes (but not playbook includes). This adds the ability to loop over the set of tasks in one shot.
|
||||
Ansible by default sets the loop variable `item` for each loop, which causes these nested loops to overwrite the value of `item` from the "outer" loops.
|
||||
As of Ansible 2.1, the `loop_control` option can be used to specify the name of the variable to be used for the loop::
|
||||
|
||||
# main.yml
|
||||
- include: inner.yml
|
||||
- include_tasks: inner.yml
|
||||
with_items:
|
||||
loop:
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
|
@ -754,7 +242,7 @@ As of Ansible 2.1, the `loop_control` option can be used to specify the name of
|
|||
# inner.yml
|
||||
- debug:
|
||||
msg: "outer item={{ outer_item }} inner item={{ item }}"
|
||||
with_items:
|
||||
loop:
|
||||
- a
|
||||
- b
|
||||
- c
|
||||
|
@ -769,7 +257,7 @@ When using complex data structures for looping the display might get a bit too "
|
|||
digital_ocean:
|
||||
name: "{{ item.name }}"
|
||||
state: present
|
||||
with_items:
|
||||
loop:
|
||||
- name: server1
|
||||
disks: 3gb
|
||||
ram: 15Gb
|
||||
|
@ -791,7 +279,7 @@ Another option to loop control is C(pause), which allows you to control the time
|
|||
digital_ocean:
|
||||
name: "{{ item }}"
|
||||
state: present
|
||||
with_items:
|
||||
loop:
|
||||
- server1
|
||||
- server2
|
||||
loop_control:
|
||||
|
@ -808,7 +296,7 @@ for `item`::
|
|||
|
||||
# main.yml
|
||||
- include_tasks: inner.yml
|
||||
with_items:
|
||||
loop:
|
||||
- 1
|
||||
- 2
|
||||
- 3
|
||||
|
@ -819,19 +307,12 @@ for `item`::
|
|||
|
||||
- debug:
|
||||
msg: "outer item={{ outer_item }} inner item={{ item }}"
|
||||
with_items:
|
||||
loop:
|
||||
- a
|
||||
- b
|
||||
- c
|
||||
|
||||
|
||||
.. _writing_your_own_iterators:
|
||||
|
||||
Writing Your Own Iterators
|
||||
``````````````````````````
|
||||
|
||||
While you ordinarily shouldn't have to, should you wish to write your own ways to loop over arbitrary data structures, you can read :doc:`dev_guide/developing_plugins` for some starter
|
||||
information. Each of the above features are implemented as plugins in ansible, so there are many implementations to reference.
|
||||
.. note:: `include` is deprecated, you should be using `include_tasks`, `import_tasks`, `import_play` instead.
|
||||
|
||||
.. seealso::
|
||||
|
||||
|
|
|
@ -36,9 +36,9 @@ using the :func:`list <jinja2:list>` filter whenever using :meth:`dict.keys`,
|
|||
- debug:
|
||||
msg: '{{ item }}'
|
||||
# Only works with Python 2
|
||||
#with_items: "{{ hosts.keys() }}"
|
||||
#loop: "{{ hosts.keys() }}"
|
||||
# Works with both Python 2 and Python 3
|
||||
with_items: "{{ hosts.keys() | list }}"
|
||||
loop: "{{ hosts.keys() | list }}"
|
||||
|
||||
.. _pb-py-compat-iteritems:
|
||||
|
||||
|
@ -59,9 +59,9 @@ compatible with both Python2 and Python3::
|
|||
- debug:
|
||||
msg: '{{ item }}'
|
||||
# Only works with Python 2
|
||||
#with_items: "{{ hosts.iteritems() }}"
|
||||
#loop: "{{ hosts.iteritems() }}"
|
||||
# Works with both Python 2 and Python 3
|
||||
with_items: "{{ hosts.items() | list }}"
|
||||
loop: "{{ hosts.items() | list }}"
|
||||
|
||||
.. seealso::
|
||||
* The :ref:`pb-py-compat-dict-views` entry for information on
|
||||
|
|
|
@ -12,7 +12,7 @@ Example::
|
|||
tasks:
|
||||
|
||||
- yum: name={{ item }} state=installed
|
||||
with_items:
|
||||
loop:
|
||||
- httpd
|
||||
- memcached
|
||||
tags:
|
||||
|
|
|
@ -189,20 +189,20 @@ class TaskExecutor:
|
|||
|
||||
templar = Templar(loader=self._loader, shared_loader_obj=self._shared_loader_obj, variables=self._job_vars)
|
||||
items = None
|
||||
if self._task.loop:
|
||||
if self._task.loop in self._shared_loader_obj.lookup_loader:
|
||||
if self._task.loop_with:
|
||||
if self._task.loop_with in self._shared_loader_obj.lookup_loader:
|
||||
fail = True
|
||||
if self._task.loop == 'first_found':
|
||||
if self._task.loop_with == 'first_found':
|
||||
# first_found loops are special. If the item is undefined then we want to fall through to the next value rather than failing.
|
||||
fail = False
|
||||
|
||||
loop_terms = listify_lookup_plugin_terms(terms=self._task.loop_args, templar=templar, loader=self._loader, fail_on_undefined=fail,
|
||||
loop_terms = listify_lookup_plugin_terms(terms=self._task.loop, templar=templar, loader=self._loader, fail_on_undefined=fail,
|
||||
convert_bare=False)
|
||||
if not fail:
|
||||
loop_terms = [t for t in loop_terms if not templar._contains_vars(t)]
|
||||
|
||||
# get lookup
|
||||
mylookup = self._shared_loader_obj.lookup_loader.get(self._task.loop, loader=self._loader, templar=templar)
|
||||
mylookup = self._shared_loader_obj.lookup_loader.get(self._task.loop_with, loader=self._loader, templar=templar)
|
||||
|
||||
# give lookup task 'context' for subdir (mostly needed for first_found)
|
||||
for subdir in ['template', 'var', 'file']: # TODO: move this to constants?
|
||||
|
@ -213,7 +213,12 @@ class TaskExecutor:
|
|||
# run lookup
|
||||
items = mylookup.run(terms=loop_terms, variables=self._job_vars, wantlist=True)
|
||||
else:
|
||||
raise AnsibleError("Unexpected failure in finding the lookup named '%s' in the available lookup plugins" % self._task.loop)
|
||||
raise AnsibleError("Unexpected failure in finding the lookup named '%s' in the available lookup plugins" % self._task.loop_with)
|
||||
|
||||
elif self._task.loop:
|
||||
items = templar.template(self._task.loop)
|
||||
if not isinstance(items, list):
|
||||
raise AnsibleError("Invalid data passed to 'loop' it requires a list, got this instead: %s" % items)
|
||||
|
||||
# now we restore any old job variables that may have been modified,
|
||||
# and delete them if they were in the play context vars but not in
|
||||
|
@ -264,7 +269,10 @@ class TaskExecutor:
|
|||
u" to something else to avoid variable collisions and unexpected behavior." % loop_var)
|
||||
|
||||
ran_once = False
|
||||
items = self._squash_items(items, loop_var, task_vars)
|
||||
if self._task.loop_with:
|
||||
# Only squash with 'with_:' not with the 'loop:', 'magic' squashing can be removed once with_ loops are
|
||||
items = self._squash_items(items, loop_var, task_vars)
|
||||
|
||||
for item in items:
|
||||
task_vars[loop_var] = item
|
||||
|
||||
|
|
|
@ -75,8 +75,7 @@ class Task(Base, Conditional, Taggable, Become):
|
|||
_delegate_to = FieldAttribute(isa='string')
|
||||
_delegate_facts = FieldAttribute(isa='bool', default=False)
|
||||
_failed_when = FieldAttribute(isa='list', default=[])
|
||||
_loop = FieldAttribute(isa='string', private=True, inherit=False)
|
||||
_loop_args = FieldAttribute(isa='list', private=True, inherit=False)
|
||||
_loop = FieldAttribute()
|
||||
_loop_control = FieldAttribute(isa='class', class_type=LoopControl, inherit=False)
|
||||
_name = FieldAttribute(isa='string', default='')
|
||||
_notify = FieldAttribute(isa='list')
|
||||
|
@ -85,6 +84,9 @@ class Task(Base, Conditional, Taggable, Become):
|
|||
_retries = FieldAttribute(isa='int', default=3)
|
||||
_until = FieldAttribute(isa='list', default=[])
|
||||
|
||||
# deprecated, used to be loop and loop_args but loop has been repurposed
|
||||
_loop_with = FieldAttribute(isa='string', private=True, inherit=False)
|
||||
|
||||
def __init__(self, block=None, role=None, task_include=None):
|
||||
''' constructors a task, without the Task.load classmethod, it will be pretty blank '''
|
||||
|
||||
|
@ -145,16 +147,17 @@ class Task(Base, Conditional, Taggable, Become):
|
|||
else:
|
||||
return "TASK: %s" % self.get_name()
|
||||
|
||||
def _preprocess_loop(self, ds, new_ds, k, v):
|
||||
def _preprocess_with_loop(self, ds, new_ds, k, v):
|
||||
''' take a lookup plugin name and store it correctly '''
|
||||
|
||||
loop_name = k.replace("with_", "")
|
||||
if new_ds.get('loop') is not None:
|
||||
if new_ds.get('loop') is not None or new_ds.get('loop_with') is not None:
|
||||
raise AnsibleError("duplicate loop in task: %s" % loop_name, obj=ds)
|
||||
if v is None:
|
||||
raise AnsibleError("you must specify a value when using %s" % k, obj=ds)
|
||||
new_ds['loop'] = loop_name
|
||||
new_ds['loop_args'] = v
|
||||
new_ds['loop_with'] = loop_name
|
||||
new_ds['loop'] = v
|
||||
display.deprecated("with_ type loops are being phased out, use the 'loop' keyword instead", version="2.9")
|
||||
|
||||
def preprocess_data(self, ds):
|
||||
'''
|
||||
|
@ -210,7 +213,7 @@ class Task(Base, Conditional, Taggable, Become):
|
|||
continue
|
||||
elif k.replace("with_", "") in lookup_loader:
|
||||
# transform into loop property
|
||||
self._preprocess_loop(ds, new_ds, k, v)
|
||||
self._preprocess_with_loop(ds, new_ds, k, v)
|
||||
else:
|
||||
# pre-2.0 syntax allowed variables for include statements at the top level of the task,
|
||||
# so we move those into the 'vars' dictionary here, and show a deprecation message
|
||||
|
@ -248,9 +251,9 @@ class Task(Base, Conditional, Taggable, Become):
|
|||
|
||||
super(Task, self).post_validate(templar)
|
||||
|
||||
def _post_validate_loop_args(self, attr, value, templar):
|
||||
def _post_validate_loop(self, attr, value, templar):
|
||||
'''
|
||||
Override post validation for the loop args field, which is templated
|
||||
Override post validation for the loop field, which is templated
|
||||
specially in the TaskExecutor class when evaluating loops.
|
||||
'''
|
||||
return value
|
||||
|
|
|
@ -522,7 +522,7 @@ class VariableManager:
|
|||
if task.loop is not None:
|
||||
if task.loop in lookup_loader:
|
||||
try:
|
||||
loop_terms = listify_lookup_plugin_terms(terms=task.loop_args, templar=templar,
|
||||
loop_terms = listify_lookup_plugin_terms(terms=task.loop, templar=templar,
|
||||
loader=self._loader, fail_on_undefined=True, convert_bare=False)
|
||||
items = lookup_loader.get(task.loop, loader=self._loader, templar=templar).run(terms=loop_terms, variables=vars_copy)
|
||||
except AnsibleUndefinedVariable:
|
||||
|
|
|
@ -106,8 +106,8 @@ class TestTaskExecutor(unittest.TestCase):
|
|||
mock_host = MagicMock()
|
||||
|
||||
mock_task = MagicMock()
|
||||
mock_task.loop = 'items'
|
||||
mock_task.loop_args = ['a', 'b', 'c']
|
||||
mock_task.loop_with = 'items'
|
||||
mock_task.loop = ['a', 'b', 'c']
|
||||
|
||||
mock_play_context = MagicMock()
|
||||
|
||||
|
|
Loading…
Reference in a new issue