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:
Brian Coca 2017-09-16 23:32:34 -04:00 committed by Brian Coca
parent bd17edd5ed
commit d84df2405d
24 changed files with 157 additions and 1211 deletions

View file

@ -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):

View file

@ -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]

View file

@ -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."

View file

@ -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:

View file

@ -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

View file

@ -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::

View file

@ -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

View file

@ -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

View file

@ -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.

View file

@ -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

View file

@ -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.

View file

@ -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

View file

@ -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

View file

@ -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::

View file

@ -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`.

View file

@ -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}"

View file

@ -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::

View file

@ -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::

View file

@ -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

View file

@ -12,7 +12,7 @@ Example::
tasks:
- yum: name={{ item }} state=installed
with_items:
loop:
- httpd
- memcached
tags:

View file

@ -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

View file

@ -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

View file

@ -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:

View file

@ -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()