Ansible, part V – Loops, Conditional Statements and Handlers

In an earlier article, we talked about collecting facts from managed nodes and about how they can be used in our playbooks. We also showed how you can refer to them in the playbook.

Going one step further, we’ll look at how they can be used to create conditional tasks. The “when” clause is used for this purpose. It can also be used to condition subsequent tasks on the basis of previous results.

The “when” clause uses Jinja2’s raw syntax, thus enabling the use of extensive expressions using Jinja2’s tests (comparisons) and filters (data manipulation) as well as logical expressions such as “OR” or “AND“. It also means that we have direct access to the variables, without the need for double curly braces “{{}}“.

This time, we have defined a list for the “msg” option of the “debug” module. It is worth comparing its result with what it looked like in the previous article. A bit lower, we also used the “var” option of the “debug” module, as well as the “register” parameter. It is worth revisiting this information in the previous article, if the application of these parameters is not clear to you.

There is no HOST variable declared anywhere in our playbook, as well as in the inventory.

The HOST variable is defined during our playbook call with the “–extra-vars” option.

The result of the last two tasks was additionally conditioned in our playbook with the “when” clause. The penultimate task was completed only for “10.8.232.122” and the last for “10.8.232.122” and “10.8.232.124“. It is also worth noting, that the last task gives a different result for the two hosts as it depends on what happened in the penultimate task.

The “when” clause is useful in creating universal playbooks, that condition individual tasks on the type or state of the node on which they will work. For example, if the node’s operating system belongs to the “Debian” family, we will use the “apt” module to install the software, and if it is “RedHat“, then the “yum” module. In our example, we will additionally use the “AND” logical condition to restrict the installation to only one node with a specific IP address.

We are working on the same inventory that contains four hosts. We will now use the “–limit” option of the “ansible-playbook” command to restrict playbook execution to only two of them.

As a result of our playbook, the software was installed only on 1 host “10.8.232.123“.

More about conditional and boolean expressions can be found in the Ansible documentation.
—-

We already know from the previous article, that in Jinja2 templates we can refer to variable values by using double curly braces “{{}}“. Comments can also be added to Jinja2 templates. They are placed inside curly braces, between the hash characters “{# #}“. Moreover, conditional expressions can also be used in Jinja2 templates. They are placed inside curly braces with percent signs -“{%%}“. These expressions can be quite complex, and in addition to comparisons, they can include logical AND and OR operators.

The “{%elif%}” and “{%else%}” sections are optional. However, the “{%if%}” and “{%endif%}” sections are mandatory. As a result of the playbook execution, only the lines that meet the conditions will be inserted into the target file from our Jinja2 template.

After establishing the HTTP connection, you can see the content of the “index.html” file, which was created from our template “index.html.j2“.
—-

Loops can also be used at the level of YAML and Jinja2 templates. They enable the cyclical repetition of the same tasks on different elements. Usually they stop working after all items on the list have passed through or as a result of some event or condition being met.

Below you can see the use of a “for” loop inside the Jinja2 template. For this purpose, the expression “for .. in .. endfor” is used appropriately inside curly braces and percent signs -“{%%}“.

During fact gathering, a list “ansible_dns.nameservers” was built for each node, containing all DNS servers configured on it. In the Jinja2 template, we used the expression “for” to list all DNS servers on this list. While the Jinja2 template has changed, our playbook looks exactly the same as in the previous example. The result of his work can be seen below.

After establishing the HTTP connection, you can see the content of the “index.html” file, which was created from our template “index.html.j2“.
—-

In the previous article, we showed the first use of loops inside a playbook. The “loop” or “until” statements are recommended for handling such loops. There is also the expression “with_ <lookup>“, which was more widely used in the past but should be avoided now.

The use of loops inside playbooks is quite wide. We use them when we want to install more than one package, unblock more than one port number, set up more than one user account, create more than one VLAN or adjust the permissions of more than one file. What is common to all these tasks is the “more than one” direction. Thus, we will use the loop when we want to perform a task more than once, but not necessarily with the same variable values.

We use the expression “until“, when the loop must run until a certain result is obtained, and the expression “loop“, when it is to run for each item in the specified list. In practice, the “loop” is much more used, so we’ll go through a few different examples of how it is used.

We’ll start by using a “loop” for simple lists. By default, the variable “{{item}}” is a placeholder. It takes sequentially the values of the elements from the list, which is a parameter of the “loop“.

When we want to refer to the list defined in thevars” section or in a special YAML file with variables, we need to refer to it by double brace “{{ }}“. It is also possible to define the list directly in theloopexpression.

Above you can see the result of our playbook run. It displays all the items in the defined lists. For each of them, the “loop” ran as many times as the “msg” messages can be seen.
—-

There are times when we want to loop through a dictionary object. The dictionary groups together several variables, called keys, with assigned values. Each of these groups usually parameterizes one object or element. In our example it will be a VLAN. Since the expression “loop” expects a list argument, we will apply the “dict2items” filter to manipulate the data. It will convert our dictionary into a list that the “loop” can handle.

By default, inside the loop, successive values are assigned to the “item” variable. In our example, we changed this name to “vlan” using the “loop_var” variable of the “loop_control” section. The “loop_control” section is used to manipulate the behavior of a loop, including the amount and type of information it provides. See the Ansible documentation for more information on “loop_control“.

In this example, the name “vlan” seems more intuitive. However, in some cases we may have to change this name due to overlapping loops and collisions in names. An example would be to use a job in the “loop” that uses the loop itself. Then the variable with the same name – “item” – can not be used in the outer and inner loops at the same time.

Above you can see the effect of our playbook run and all elements of the defined dictionary with VLAN configuration.
—-

Another use of “loop” is to perform operations on successive files of a given directory.

In our example, we will use a directory with the following content:

To pass a list of files to the input of the “loop“, we will use the “lookup” function. Its first argument will be the “fileglob” option, which lists the files in the specified directory, that match the pattern specified in the second argument of this function (files only, not directories). The search is carried out locally at the management station, where the playbook was launched. By default, the output will be a list of absolute file paths with the comma “,” as the separator. If we want the output to get a typical list, set the “wantlist” variable inside this function to “true“. This is shown in the example below.

You can also perform operations on files located on a managed node. For this purpose, use the “find” module. More about it can be found in the Ansible documentation.
—-

The loop “loop” also allows you to perform specific operations for each line of the specified file.

To pass the contents of a file to the input of the “loop“, we will use the “lookup” function. Its first argument will be the ‘file‘ option, which lists the contents of the file, and the second argument will be the location of this file. If you want to get a list in which each line of the file will be a separate element, use the Python method “splitlines ()“.

The list of available methods is very large. We used some useful operations on the text:

  • swapcase()” reverses the case of letters, so lowercase becomes uppercase, and uppercase becomes lowercase.
  • lower ()” makes all letters lowercase.
  • capitalize()” makes only the first letter of the entire string uppercase.
  • split()” splits the text based on the given separator.
  • count()” counts the number of occurrences of a given pattern.

Below, we have displayed the original text first and then its variations successively using various methods. Due to the use of the “>” operator, this is 1 line of continuous text.

There are many more uses for loops, so see the Ansible documentation for more.
—-

Handlers are common tasks, whose execution is triggered by state change notifications from other tasks. Using them translates into better efficiency and performance of the playbook. The task in the hander will only be executed once, no matter how many times it is triggered by various other tasks. Additionally, if for a given node the task in the hander does not receive any notification, it will not be started at all.

In our example, the handler list was defined with a job using the “yum” module. The “notify” section serves this purpose, where the names of the handlers to be notified, when a change is made are indicated. Tasks that handle handles are defined in a special “handlers” section.

Thanks to the handlers in our playbook, if a node has already installed the “httpd” package, it will not try to do anything else. In our case, it will not try to open ports for HTTP and HTTPS traffic and start “httpd” services.

However, if the node does not have the “httpd” package, the appropriate packages will be installed, which in turn will trigger a notify and run handlers, whose task is to ensure that the “httpd” service is running and working after reboot, and the system firewall allowed HTTP and HTTPS traffic to it.

In our example you can see that only 2 out of 4 servers did not have the “httpd” package installed and only for them the additional tasks from the “handlers” section were run.

Author:

CEO, Senior Network Engineer

Marcin Ślęczek

CEO, Network Engineer and System Administrator at networkers.pl Sp. z o.o., which deals in the sale and implementation of solutions, hardware and software for IT systems and data centers.