Ansible loops
The following loops are supported by Ansible:
- Simple loops: A simple loop is a list of items that Ansible reads and iterate over. They are defined by providing a list of items to the
with_items
keyword. Consider the following snippet that uses the yum module twice in order to install two packages:
1 2 3 4 5 6 |
- yum: name: postfix state: latest - yum: name: dovecot state: latest |
These two similar tasks using the yum module can be rewritten with a simple loop so that only one task is needed to install both packages:
1 2 3 4 5 6 |
- yum: name: "{{ item }}" state: latest with_items: - postfix - dovecot |
The previous code can be replaced by having the packages inside an array called with the with_items
keyword; the following playbook shows how to pass the array mail_services
as an argument; the module will loop over the array to retrieve the name of the packages to install:
1 2 3 4 5 6 7 8 9 10 |
vars: mail_services: - postfix - dovecot tasks: - yum: name: "{{ item }}" state: latest with_items: "{{ mail_services }}" |
with_items example
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
[miro@controlnode playbooks]$ cat playbook3.yml --- - hosts: localhost become: yes tasks: - name: create users user: name: "{{item}}" with_items: - sam - john - bob [miro@controlnode playbooks]$ id sam id: sam: nie ma takiego użytkownika [miro@controlnode playbooks]$ id john id: john: nie ma takiego użytkownika [miro@controlnode playbooks]$ id bob id: bob: nie ma takiego użytkownika [miro@controlnode playbooks]$ ansible-playbook playbook3.yml PLAY [localhost] ******************************************************************************************************************************* TASK [Gathering Facts] ************************************************************************************************************************* ok: [localhost] TASK [create users] **************************************************************************************************************************** changed: [localhost] => (item=sam) changed: [localhost] => (item=john) changed: [localhost] => (item=bob) PLAY RECAP ************************************************************************************************************************************* localhost : ok=2 changed=1 unreachable=0 failed=0 [miro@controlnode playbooks]$ id sam; id john; id bob uid=6004(sam) gid=6005(sam) grupy=6005(sam) uid=6005(john) gid=6006(john) grupy=6006(john) uid=6006(bob) gid=6007(bob) grupy=6007(bob) |
- List of hashes: When passing arrays as arguments, the array can be a list of hashes. The following snippet shows how a multidimensional array (an array with key-pair values) is passed to the user module in order to customize both the name and the group:
1 2 3 4 5 6 7 |
- user: name: "{{ item.name }}" state: present groups: "{{ item.groups }}" with_items: - { name: 'jane', groups: 'wheel' } - { name: 'joe', groups: 'root' } |
- Nested loops: loops inside of loops called with the
with_nested
keyword. When nested loops are used, Ansible iterates over the first array as long as there are values in it. For example, when multiple MySQL privileges are
required for multiple users, administrators can create a multidimensional array and invoke it with the with_nested keyword. The following snippet shows how to use the mysql_user module in a nested loop to grant two users a set of three privileges:
12345678- mysql_user:name: "{{ item[0] }}"priv: "{{ item[1] }}.*:ALL"append_privs: yespassword: redhatwith_nested:- [ 'joe', 'jane' ]- [ 'clientdb', 'employeedb', 'providerdb' ]
In the previous example, the name of the users to create can also be an array defined in a variable. The following snippet shows how users are defined inside an array and passed as thefirst item of the array:
12345678910111213vars:users:- joe- janetasks:- mysql_user:name: "{{ item[0] }}"priv: "{{ item[1] }}.*:ALL"append_privs: yespassword: redhatwith_nested:- "{{ users }}"- [ 'clientdb', 'employeedb', 'providerdb' ]
The following table shows some additional types of loops supported by Ansible.
Loop | Keyword Description |
with_file |
Takes a list of control node file names. item is set to the content of each file in sequence. |
with_fileglob |
Takes a file name globbing pattern. item is set to each file in a directory on the control node that matches that pattern, in sequence, non-recursively. |
with_sequence |
Generates a sequence of items in increasing numerical order. Can take start and end arguments which have a decimal, octal, or hexadecimal integer value. |
with_random_choices |
Takes a list. item is set to one of the list items at random. |
with_file example
1 2 3 4 5 6 7 8 9 10 11 12 |
--- - hosts: all tasks: - name: copy files copy: src: "{{item}}" dest: "/home/user/{{item}}" mode: 400 with_file: - filel - file2 - file3 |
To capture the output of a module that uses an array, the register
keyword can be used with an array. Ansible will save the output in the variable. This can be useful if administrators want to review the result of the execution of a module. The following snippet shows how to register the content of the array after the iteration:
1 2 3 4 5 |
- shell: echo "{{ item }}" with_items: - one - two register: echo |
The echo will then contain the following information:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
{ "changed": true, "msg": "All items completed", "results": [ { "changed": true, "cmd": "echo \"one\" ", "delta": "0:00:00.003110", "end": "2013-12-19 12:00:05.187153", "invocation": { "module_args": "echo \"one\"", "module_name": "shell" }, "item": "one", "rc": 0, "start": "2013-12-19 12:00:05.184043", "stderr": "", "stdout": "one" }, { "changed": true, "cmd": "echo \"two\" ", "delta": "0:00:00.002920", "end": "2013-12-19 12:00:05.245502", "invocation": { "module_args": "echo \"two\"", "module_name": "shell" }, "item": "two", "rc": 0, "start": "2013-12-19 12:00:05.242582", "stderr": "", "stdout": "two" } ] } |
Conditionals
Ansible can use conditionals to execute tasks or plays when certain conditions are met. For example, a conditional can be used to determine the available memory on a managed host before Ansible installs or configures a service.
Ansible conditionals operators
Operator | Example |
Equal | "{{ max_memory }} == 512" |
Less than | "{{ min_memory }} < 128" |
Greater than | "{{ min_memory }} > 256" |
Less than or equal to | "{{ min_memory }} <= 256" |
Greater than or equal to | "{{ min_memory }} >= 512" |
Not equal to | "{{ min_memory }} != 512" |
Variable exists | "{{ min_memory }} is defined" |
Variable does not exist | "{{ min_memory }} is not defined" |
Variable is set to 1, True, or yes | "{{ available_memory }}" |
Variable is set to 0, False, or no | "not {{ available_memory }}" |
Value is present in a variable or an array | "{{ users }} in users["db_admins"]" |
When statement
To implement a conditional on an element, the when statement must be used, followed by the condition to test. When the statement is present, Ansible will evaluate it prior to executing the task. The following snippet shows a basic implementation of a when statement. Before creating the user db_admin, Ansible must ensure the managed host is part of the databases group:
1 2 3 4 |
- name: Create the database admin user: name: db_admin when: inventory_hostname in groups["databases"] |
Important
Notice the placement of the when statement. Because the when statement is not a module variable, it must be placed “outside” the module by being indented at the top level of the task. The when statement does not have to be at the top of the task. This breaks from the “top-down” ordering that is normal for Ansible.
when example
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
[miro@controlnode playbooks]$ cat playbook4.yml --- - hosts: labservers become: yes tasks: - name: edit index lineinfile: path: /var/www/html/index.html line: "I'm back!!" when: - ansible_hostname == "managedhost2" [miro@controlnode playbooks]$ ansible-playbook playbook4.yml PLAY [labservers] ****************************************************************************************************************************** TASK [Gathering Facts] ************************************************************************************************************************* ok: [managedhost2.example.com] ok: [managedhost1.example.com] TASK [edit index] ****************************************************************************************************************************** skipping: [managedhost1.example.com] changed: [managedhost2.example.com] PLAY RECAP ************************************************************************************************************************************* managedhost1.example.com : ok=1 changed=0 unreachable=0 failed=0 managedhost2.example.com : ok=2 changed=1 unreachable=0 failed=0 [miro@controlnode playbooks]$ curl managedhost2 Hello World ! I'm back!! |
Multiple conditions
One when statement can be used to evaluate multiple values. To do so, conditionals can be combined with the and
and or
keywords or grouped with parentheses.
- With the and operation, both conditions have to be true for the entire conditional statement to be met.
1 |
ansible_kernel == 3.10.0-327.el7.x86_64 and inventory_hostname in groups['staging'] |
- If a conditional statement should be met when either condition is true, then the or statement should be used.
1 |
ansible_distribution == "RedHat" or ansible_distribution == "Fedora" |
- More complex conditional statements can be clearly expressed through grouping conditions with parentheses to ensure that they are correctly interpreted.
1 2 |
(ansible_distribution == "RedHat" and ansible_distribution_major_version == 7) or (ansible_distribution == "Fedora" and ansible_distribution_major_version == 23) |
Loops and conditionals can be combined. In the following example, the mariadb-server package will be installed by the yum module if there is a file system mounted on / with more than 300 MB free. The ansible_mounts
fact is a list of dictionaries, each one representing facts about one mounted file system. The loop iterates over each dictionary in the list, and the conditional
statement is not met unless a dictionary is found representing a mounted file system where both conditions are true.
1 2 3 4 5 6 |
- name: install mariadb-server if enough space on root yum: name: mariadb-server state: latest with_items: "{{ ansible_mounts }}" when: item.mount == "/" and item.size_available > 300000000 |
Important
When combining when
with with_items
, be aware that the when
statement is
processed for each item.
Here is another example combining conditionals and registered
variables. The following annotated playbook will restart the httpd service only if the postfix service is running.
1 2 3 4 5 6 7 8 9 10 11 |
- hosts: all tasks: - name: Postfix server status command: /usr/bin/systemctl is-active postfix (1) ignore_errors: yes (2) register: result (3) - name: Restart Apache HTTPD if Postfix running service: name: httpd state: restarted when: result.rc == 0 (4) |
1. Is Postfix running or not?
2. If it is not running and the command “fails”, do not stop processing
3. Saves information on the module’s result in a variable named result
4. Evaluates the output of the Postfix task. If the exit code of the systemctl
command is 0, then Postfix is active and this task will restart the httpd service.
Using Booleans
Booleans are variables that take one of two possible values, which can be expressed as:
• True or Yes or 1
• False or No or 0
- Booleans can be used as a simple switch to enable or disable tasks. To enable the task:
1 2 3 4 5 6 7 8 9 |
--- - hosts: all vars: my_service: True tasks: - name: Installs a package yum: name: httpd when: my_service |
- To disable the task:
1 2 3 4 5 6 7 8 9 |
--- - hosts: all vars: my_service: False tasks: - name: Installs a package yum: name: httpd when: my_service |
Exercise: Constructing Flow Control
Create a task file named configure_database.yml
. This will define the tasks to install the extra packages, update /etc/my.cnf
from a copy stored on a web site, and start mariadb on the managed hosts. The include file can and will use the variables you defined in the playbook.yml
file and inventory. The get_url
module will need to set force=yes
so that the my.cnf
file is updated even if it already exists on the managed host, and will need to set correct permissions as well as SELinux contexts on the /etc/my.cnf
file. The file should read as follows:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
- yum: name: "{{ extra_packages }}" - get_url: url: "http://materials.example.com/task_control/my.cnf" dest: "{{ configure_database_path }}" owner: mysql group: mysql mode: 0644 seuser: system_u setype: mysqld_etc_t force: yes - service: name: "{{ db_service }}" state: started enabled: true |
In the same directory, create the playbook.yml
playbook. Define a list variable, db_users
, that consists of a list of two users, db_admin
and db_user
. Add a configure_database_path
variable set to the file /etc/my.cnf
. Create a task that uses a loop to create the users only if the managed host belongs to the databases host group. In the playbook, add a task that uses the db_package
variable to install the database software, only if the variable has been defined. Create a task to do basic configuration of the database. The task will run only when configure_database_path
is defined. This task should include the configure_database.yml
task file and define a local array extra_packages
which will be used to specify additional packages needed for this configuration. Set that list variable to include a list of three packages: mariadb-bench, mariadb-libs, and mariadb-test.
The final playbook.yml should now read in its entirety:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
--- - hosts: all vars: db_package: mariadb-server db_service: mariadb db_users: - db_admin - db_user configure_database_path: /etc/my.cnf tasks: - name: Create the MariaDB users user: name: "{{ item }}" with_items: "{{ db_users }}" when: inventory_hostname in groups['databases'] - name: Install the database server yum: name: "{{ db_package }}" when: db_package is defined - name: Configure the database software include: configure_database.yml vars: extra_packages: - mariadb-bench - mariadb-libs - mariadb-test when: configure_database_path is defined |
Run the playbook to install and configure the database on the managed hosts.
1 2 3 4 5 6 7 8 |
[student@workstation dev-flowcontrol]$ ansible-playbook playbook.yml PLAY ************************************************************************ TASK [setup] **************************************************************** ok: [servera.lab.example.com] ... Output omitted ... TASK [Includes the configuration] ******************************************* included: /home/student/dev-flowcontrol/configure_database.yml for servera.lab.example.com |
The output confirms the task file has been successfully included and executed.
Manually verify that the necessary packages have been installed on servera, that the /etc/my.cnf
file is in place with the correct permissions, and that the two users have been created. Use an ad hoc command from workstation to servera to confirm the packages have been installed.
1 2 3 4 5 6 7 |
[student@workstation dev-flowcontrol]$ ansible all -a 'yum list installed mariadb-bench mariadb-libs mariadb-test' servera.lab.example.com | SUCCESS | rc=0 >> Loaded plugins: langpacks, search-disabled-repos Installed Packages mariadb-bench.x86_64 1:5.5.44-2.el7 @rhel_dvd mariadb-libs.x86_64 1:5.5.44-2.el7 installed mariadb-test.x86_64 1:5.5.44-2.el7 @rhel_dvd |
Confirm the my.cnf
file has been successfully copied under /etc/
.
1 2 3 |
[student@workstation dev-flowcontrol]$ ansible all -a 'grep Ansible /etc/my.cnf' servera.lab.example.com | SUCCESS | rc=0 >> # Ansible file |
Confirm the two users have been created.
1 2 3 4 5 6 7 |
[student@workstation dev-flowcontrol]$ ansible all -a 'id db_user' servera.lab.example.com | SUCCESS | rc=0 >> uid=1003(db_user) gid=1003(db_user) groups=1003(db_user) [student@workstation dev-flowcontrol]$ ansible all -a 'id db_admin' servera.lab.example.com | SUCCESS | rc=0 >> uid=1002(db_admin) gid=1002(db_admin) groups=1002(db_admin) |