Today I'm going to show you an inbuilt debugger tool that you can use in Ansible to help troubleshoot your tasks and playbooks. As per the Ansible docs, the debugger tool allows you to:
… fix errors during execution instead of editing your Playbook and running it again to see if your change worked. You have access to all of the features of the debugger in the context of the task …
In other words, it is similar to the Python PDB tool (minus a few features) in that you can step through your code at various points and see your variables, etc., each time.
Note: Our example will be based on the following Playbook, which has a simple mistake added!
---
- name: Create Interfaces
hosts: spine
gather_facts: false
debugger: on_failed
tasks:
- name: Create L3 interfaces
cisco.nxos.nxos_l3_interfaces:
config:
- name: "{{ item.name }}"
ipv4:
- address: "{{ item.add }}"
state: "merged"
loop: "{{ interfaces }}"
Enabling the Debugger
To use the debugger, we can enable it in a number of ways, such as via the Ansible config file, via an environment variable, or at a Play or Task level. In addition, you can also define when it will be invoked, i.e., always
, on_failed
, etc. (full details here).
For our short example, we will enable this at a Play level:
---
- name: Create Interfaces
hosts: spine
gather_facts: false
debugger: on_failed
…
At the point we run our Playbook, we will be placed into the debugger shell:
$ ansible-playbook -i inventory.yaml playbooks/pb_create_interfaces.yaml
PLAY [Create Interfaces] *****************************************************************************************************************************
TASK [Create L3 interfaces] **************************************************************************************************************************
fatal: [spine1-nxos]: FAILED! =>
...
The offending line appears to be:
tasks:
- name: Create L3 interfaces
^ here
[spine1-nxos] TASK: Create L3 interfaces (debug)>
At this point, we have a number of options, such as printing the values, changing the values and also stepping to the next step in the Play. Let's look at some examples...
Printing Values
When it comes to print values, we have a few options; we can print the:
- Task name via
p task
- Task input arguments via
p task.args
- Task variables via
p task_vars
.
Here's an example:
# Printing the task name.
[spine1-nxos] TASK: Create L3 interfaces (debug)> p task
TASK: Create L3 interfaces
# Printing the task arguments.
[spine1-nxos] TASK: Create L3 interfaces (debug)> p task.args
{'config': [{'ipv4': [{'address': '{{ item.add }}'}],
'name': '{{ item.name }}'}],
'state': 'merged'}
# Printing the variables
[spine1-nxos] TASK: Create L3 interfaces (debug)> p task_vars
...
'groups': {'all': ['spine1-nxos',
'spine2-nxos',
'leaf1-ios',
'leaf2-ios',
'leaf3-junos',
'leaf4-junos',
'leaf5-eos',
'leaf6-eos'],
'leaf': ['leaf1-ios',
'leaf2-ios',
'leaf3-junos',
'leaf4-junos',
'leaf5-eos',
'leaf6-eos'],
...
'interfaces': [{'addr': '100.1.1.101/32', 'name': 'loopback111'},
{'addr': '100.1.1.102/32', 'name': 'loopback102'}],
'inventory_dir': '/home/rick/development/ansible-debugging',
'inventory_file': '/home/rick/development/ansible-debugging/inventory.yaml',
'inventory_hostname': 'spine1-nxos',
'inventory_hostname_short': 'spine1-nxos',
'omit': '__omit_place_holder__f4887f2c17be2705a5b03e8aea973407629f6113',
'play_hosts': ['spine1-nxos', 'spine2-nxos'],
'playbook_dir': '/home/rick/development/ansible-debugging/playbooks',
'role_names': []}}
# Printing a key from within the variables.
[spine1-nxos] TASK: Create L3 interfaces (debug)> p task_vars["interfaces"]
[{'addr': '100.1.1.101/32', 'name': 'loopback111'},
{'addr': '100.1.1.102/32', 'name': 'loopback102'}]
From the output, we can see that our interface key should be addr
instead of add
within our task loop.
Changing Values
Both the variables and arguments can be changed inline. To do so, select the key and then provide the new value, i.e.: task.args[key] = value
. This is exactly how you would set a key in a dictionary within Python, which makes sense since Ansible is just running Python under the hood.
Now, let's change the task input argument to the correct value:
# Change the input arguments.
spine1-nxos] TASK: Create L3 interfaces (debug)> task.args["config"][0]["ipv4"][0]["address"] = "{{ item.addr }}"
# Confirm the change.
[spine1-nxos] TASK: Create L3 interfaces (debug)> p task.args
{'config': [{'ipv4': [{'address': '{{ item.addr }}'}],
'name': '{{ item.name }}'}],
'state': 'merged'}
Stepping Through the Play
When it comes to stepping through the tasks, there are two main options. We can either:
- run the task again via
redo
orr
; or - continue and use the next task via
continue
orc
.
If we return to our example and redo
our task, we will see that spine1 is configured with the required interfaces as we have corrected the input argument. We've not done this for spine2, which is why when we then run continue
, we see another error.
[spine1-nxos] TASK: Create L3 interfaces (debug)> redo
ok: [spine1-nxos] => (item={'name': 'loopback102', 'addr': '100.1.1.102/32'})
ok: [spine1-nxos] => (item={'name': 'loopback111', 'addr': '100.1.1.101/32'})
...
fatal: [spine2-nxos]: FAILED! =>
...
The offending line appears to be:
tasks:
- name: Create L3 interfaces
^ here
[spine2-nxos] TASK: Create L3 interfaces (debug)> continue
PLAY RECAP *******************************************************************************************************************************************
spine1-nxos : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
spine2-nxos : ok=0 changed=0 unreachable=0 failed=1 skipped=0 rescued=0 ignored=0
That wraps up this post on the Ansible debugger. It's definitely a useful little tool when needing to debug your Ansible Playbooks, and it certainly warrants a place in my network automation toolbox!