📜 ⬆️ ⬇️

Network Automation with Ansible: command module

Speaking about typical network automation scenarios, one cannot do without a set of command modules. With these modules, Ansible allows you to run commands on network equipment as if you were entering them directly from the console. In this case, the output of the commands does not just skip in the terminal window to sink into oblivion, but can be saved and used later. It can be written to variables, parsed for use in subsequent tasks, or saved for future use in host variables.



The purpose of this post is to show that any repetitive network management task can be automated, and that Ansible does not just allow you to manage configurations, but helps get rid of the routine and save time.

Let us examine the basic ways of using the command network modules, including saving the output of commands using the register parameter. We will also look at how to scale to multiple network devices using hostvars and how to organize conditional execution using the wait_for parameter and three more related parameters: interval, retries, and match.

Different network platforms have their own command modules, all of which are supported at the Red Hat Ansible Engine Networking Add-on extension level:

Network platformsModules * os_command
Arista EOSeos_command
Cisco IOS / IOS-XEios_command
Cisco IOS-XRiosxr_command
Cisco NX-OSnxos_command
Juniper junosjunos_command
Vyosvyos_command

Command Module Basics


Consider a playbook that simply runs the show version command using the eos_command module:

--- - name: COMMAND MODULE PLAYBOOK hosts: eos connection: network_cli tasks: - name: EXECUTE ARISTA EOS COMMAND eos_command: commands: show version register: output - name: PRINT OUT THE OUTPUT VARIABLE debug: var: output 

Here we have two tasks and the first one uses the eos_command module with a single parameter of commands. Since we run only one command — show version — you can specify it in the same line as the commands parameter itself. If there are two or more commands, each of them should be placed on a separate line after commands :. In this example, we use the register keyword to save the output of the show version command. The register parameter (it can be used in any Ansible task) sets a variable where the output of our task will be saved so that it can be used later. In our example, this variable is called output.

The second task in our example uses the debug module to display the contents of the newly created output variable. That is, this is the same data that you would see in the command line interface of an EOS device if you entered “show version” there. The difference is that our playbook will show them in the terminal window where you run it. As you can see, the debug module makes it easy to check Ansible variables.

Here is the output of our playbook:

 PLAY [eos] ************************************************************************* TASK [execute Arista eos command] ************************************************** ok: [eos] TASK [print out the output variable] *********************************************** ok: [eos] => { "output": { "changed": false, "failed": false, "stdout": [ "Arista vEOS\nHardware version: \nSerial number: \nSystem MAC address: 0800.27ec.005e\n\nSoftware image version: 4.20.1F\nArchitecture: i386\nInternal build version: 4.20.1F-6820520.4201F\nInternal build ID: 790a11e8-5aaf-4be7-a11a-e61795d05b91\n\nUptime: 1 day, 3 hours and 23 minutes\nTotal memory: 2017324 kB\nFree memory: 1111848 kB" ], "stdout_lines": [ [ "Arista vEOS", "Hardware version: ", "Serial number: ", "System MAC address: 0800.27ec.005e", "", "Software image version: 4.20.1F", "Architecture: i386", "Internal build version: 4.20.1F-6820520.4201F", "Internal build ID: 790a11e8-5aaf-4be7-a11a-e61795d05b91", "", "Uptime: 1 day, 3 hours and 23 minutes", "Total memory: 2017324 kB", "Free memory: 1111848 kB" ] ] } } PLAY RECAP ************************************************************************* eos : ok=2 changed=0 unreachable=0 failed=0 

As you can see from the screenshot, both of our tasks have worked successfully. Since the first task uses the default message detail level, it simply says that the host eos completed the task with an ok result, highlighting the success of the execution in green. The second task, with the debug module, returns the output of the executed command, displaying the same information in two formats:


The stdout section shows the same thing that you would see in the command line interface on the device, but in the form of one long line. And the stdout_lines section breaks this output into lines so that it is easy to read. Each item in this list is a separate line in the output of the command.

Compare the output of the command on the device and in Ansible:

Command output in Arista EOSstdout_lines in ansible
eos> show vers
Arista vEOS
Hardware version:
Serial number:
System MAC address: 0800.27ec.005e

Software image version: 4.20.1F
Architecture: i386
Internal build version: 4.20.1F-6820520.4201F
Internal build ID: 790a11e8-5aaf-4be7-a11a-e61795d05b91

Uptime: 1 day, 3 hours and 56 minutes
Total memory: 2017324 kB
Free memory: 1116624 kB
"Stdout_lines": [
[
"Arista vEOS",
"Hardware version:",
"Serial number:",
"System MAC address: 0800.27ec.005e",
"",
"Software image version: 4.20.1F",
"Architecture: i386",
“Internal build version:
4.20.1F-6820520.4201F ",
“Internal build ID:
790a11e8-5aaf-4be7-a11a-e61795d05b91 ",
"",
"Uptime: 1 day, 3 hours and 23 minutes",
"Total memory: 2017324 kB",
"Free memory: 1111848 kB"
]

If you are familiar with JSON and YAML, you probably already paid attention to one oddity: stdout_lines starts with two opening brackets:

 "stdout_lines": [ [ 

Two open parentheses indicate that stdout_lines actually returns a list of strings. If we slightly change our debug task, then this chip can be used to selectively view the results of the command execution. Since there is only one list of strings in our list, this list is called zero (in fact, it is the first one, but the countdown starts from zero). Now let's see how to extract a separate line from it, for example, System MAC Address. In the output of the command, this line is the fourth in a row, but since we count from zero, we, as a result, need line 3 from list 0, in other words: output.stdout_lines [0] [3].

  - name: print out a single line of the output variable debug: var: output.stdout_lines[0][3] В ответ debug-задача возвращает именно её: TASK [print out a single line of the output variable] ****************************** ok: [eos] => { "output.stdout_lines[0][3]": "System MAC address: 0800.27ec.005e" } 

What is the point in the numbering of lists and why is it needed at all? The fact is that within one task you can run several commands, for example, like this (here we have three teams):

 --- - hosts: eos connection: network_cli tasks: - name: execute Arista eos command eos_command: commands: - show version - show ip int br - show int status register: output - name: print out command debug: var: output.stdout_lines 

Here is the output:

  "output.stdout_lines": [ [ "Arista vEOS", "Hardware version: ", "Serial number: ", "System MAC address: 0800.27ec.005e", "", "Software image version: 4.20.1F", "Architecture: i386", "Internal build version: 4.20.1F-6820520.4201F", "Internal build ID: 790a11e8-5aaf-4be7-a11a-e61795d05b91", "", "Uptime: 1 day, 4 hours and 20 minutes", "Total memory: 2017324 kB", "Free memory: 1111104 kB" ], [ "Interface IP Address Status Protocol MTU", "Ethernet1 172.16.1.1/24 up up 1500", "Management1 192.168.2.10/24 up up 1500" ], [ "Port Name Status Vlan Duplex Speed Type Flags", "Et1 connected routed full unconf EbraTestPhyPort ", "Et2 connected 1 full unconf EbraTestPhyPort ", "Et3 connected 1 full unconf EbraTestPhyPort ", "Ma1 connected routed a-full a-1G 10/100/1000" ] ] 

Here, list number zero is the output of the show version command, list number one is show ip int br output, list number two is show int status output. That is, the list number is determined by the order of command execution.

Arista EOS teamsMatching Output Lists
show versionoutput.stdout_lines [0]
show ip int broutput.stdout_lines [1]
show int statusoutput.stdout_lines [2]

Scaling command module: host variables


And what will happen if you run a playbook on multiple devices at the same time?



To preserve uniqueness, the output variable is stored as a host variable for each host in inventory. If we have three switches, and we run our playbook on them, we get the output variable for each unique host. Suppose we need an IP address from the show ip int br command for the Ethernet1 port on switch03. Since show ip int br is the second command that runs as part of the task, and the data on the Ethernet1 interface is contained in the second line of its output, we will need to write stdout_lines [1] [1]. To access the variables of a particular host, we use the hostvars keyword and search for the host we need by name.

Here is how it is done:

  - name: debug hostvar debug: var: hostvars["switch03"].output.stdout_lines[1][1] 

As a result, output contains exactly what we need:

 TASK [debug hostvar] *************************************************************** ok: [switch03] => { "hostvars[\"switch03\"].output.stdout_lines[1][1]": "Ethernet1 172.16.1.3/24 up up 1500" } 

By default, the task uses the variables of the current host, but hostvars allows you to directly access the variables of another host.

Conditions in tasks with command modules: wait_for parameter


The wait_for parameter allows you to implement a condition check immediately after the command is executed. For example, to make so that the task will be considered completed successfully, only if the output of the status verification command contains certain text. By default, the wait_for parameter is not used, so the task is run only once, as in the examples above. But if you set it explicitly, the task will be restarted until the condition is met or the limit of attempts is over (there are 10 of them by default). If you turn on command logging, you can see that in the playbook below (which is specially written so that the condition is never fulfilled) everything happens that way.

 --- - hosts: eos connection: network_cli tasks: - name: execute Arista eos command eos_command: commands: - show int status wait_for: - result[0] contains DURHAM 

This playbook will run the show int status command 10 times, since its output will never contain the string DURHAM.

You can verify this with the show logging command:

 Mar 24 20:33:52 eos Aaa: %ACCOUNTING-6-CMD: admin vty6 192.168.2.1 stop task_id=17 start_time=1521923632.5 timezone=UTC service=shell priv-lvl=15 cmd=show interfaces status Mar 24 20:33:53 eos Aaa: %ACCOUNTING-6-CMD: admin vty6 192.168.2.1 stop task_id=18 start_time=1521923633.71 timezone=UTC service=shell priv-lvl=15 cmd=show interfaces status Mar 24 20:33:54 eos Aaa: %ACCOUNTING-6-CMD: admin vty6 192.168.2.1 stop task_id=19 start_time=1521923634.81 timezone=UTC service=shell priv-lvl=15 cmd=show interfaces status Mar 24 20:33:55 eos Aaa: %ACCOUNTING-6-CMD: admin vty6 192.168.2.1 stop task_id=20 start_time=1521923635.92 timezone=UTC service=shell priv-lvl=15 cmd=show interfaces status Mar 24 20:33:56 eos Aaa: %ACCOUNTING-6-CMD: admin vty6 192.168.2.1 stop task_id=21 start_time=1521923636.99 timezone=UTC service=shell priv-lvl=15 cmd=show interfaces status Mar 24 20:33:58 eos Aaa: %ACCOUNTING-6-CMD: admin vty6 192.168.2.1 stop task_id=22 start_time=1521923638.07 timezone=UTC service=shell priv-lvl=15 cmd=show interfaces status Mar 24 20:33:59 eos Aaa: %ACCOUNTING-6-CMD: admin vty6 192.168.2.1 stop task_id=23 start_time=1521923639.22 timezone=UTC service=shell priv-lvl=15 cmd=show interfaces status Mar 24 20:34:00 eos Aaa: %ACCOUNTING-6-CMD: admin vty6 192.168.2.1 stop task_id=24 start_time=1521923640.32 timezone=UTC service=shell priv-lvl=15 cmd=show interfaces status Mar 24 20:34:01 eos Aaa: %ACCOUNTING-6-CMD: admin vty6 192.168.2.1 stop task_id=25 start_time=1521923641.4 timezone=UTC service=shell priv-lvl=15 cmd=show interfaces status Mar 24 20:34:02 eos Aaa: %ACCOUNTING-6-CMD: admin vty6 192.168.2.1 stop task_id=26 start_time=1521923642.47 timezone=UTC service=shell priv-lvl=15 cmd=show interfaces status 

Now consider an example of a real playbook in which everything is configured to establish an OSPF neighborhood (adjacency) with another device, except for the ip ospf area command. We will use this command and then use the wait_for parameter to check for the presence of the word FULL in the output: if it is there, the neighborhood has been successfully established. If after 10 attempts FULL does not appear, the task will end with an error.

 --- - hosts: eos connection: network_cli tasks: - name: turn on OSPF for interface Ethernet1 eos_config: lines: - ip ospf area 0.0.0.0 parents: interface Ethernet1 - name: execute Arista eos command eos_command: commands: - show ip ospf neigh wait_for: - result[0] contains FULL 

Let's execute this playbook using the ansible-playbook command:

 ➜ ansible-playbook ospf.yml PLAY [eos] ********************************************************************************************* TASK [turn on OSPF for interface Ethernet1] ******************************************************* changed: [eos] TASK [execute Arista eos command] **************************************************************** ok: [eos] PLAY RECAP ****************************************************************************************** eos : ok=2 changed=1 unreachable=0 failed=0 

We look at the command line and see that the playbook is executed successfully:

 eos#show ip ospf neigh Neighbor ID VRF Pri State Dead Time Address Interface 2.2.2.2 default 1 FULL/DR 00:00:33 172.16.1.2 Ethernet1 

In addition to contains, you can use the following comparison operators:


In addition, three additional parameters can be used with wait_for (described in detail in the module documentation):

ParameterDescription
intervalThe time between repetitions of the team.
retriesMax. the number of retries before the task completes with an error, or the condition is fulfilled.
matchMatch all conditions or at least one.

Let's dwell a little more on the match parameter:

  - name: execute Arista eos command eos_command: commands: - show ip ospf neigh match: any wait_for: - result[0] contains FULL - result[0] contains 172.16.1.2 

When match: any is specified, the task is considered successful if the result contains FULL or 172.16.1.2. If match: all is given, the result must contain both FULL and 172.16.1.2. By default, match: all is used, because if you prescribe several conditions, then you most likely want them to be fulfilled all, and not at least one.

When can match: any come in handy? Suppose you need to check that the data center has a two-way connection to the Internet. A data center is connected to five different Internet service providers, each of which has its own BGP connection. A playbook can check all five of these connections, and if at least one of them works, and not all five, to report that everything is in order. Just remember that any is a logical OR, and all is a logical I.

ParameterDescription
match: anyLogical "OR"
At least one condition is required
match: allLogical "and"
All conditions are required

Negative conditions: we build the reverse logic


Sometimes it’s important not what is in the conclusion, but what is not there. Here, of course, it is always tempting to use the comparison operator neq, but for some scenarios with negative conditions there are better options. For example, if you want to invert the contains operator (such as, “the output of a command should not contain this and that”), you can use the register keyword to save the output, and then process it in the next task using the when clause . Or, for example, when you need to stop a playbook when conditions are not met, simply use the fail or assert modules to exit with an error. As for the comparison operator neq, it is only useful when you can pull out the exact value from the output (for example, from a key-value pair or from JSON), and not just a string or a list of strings. Otherwise, character-by-character string comparison will be performed.

What's next


Familiarize yourself with the documentation on working with the output of commands in network modules. There are useful examples of using ge, le, and other conditions when working with output in JSON format on specific network platforms.

Source: https://habr.com/ru/post/438312/