Date created: Friday, February 14, 2020 4:26:41 PM. Last modified: Tuesday, December 26, 2023 10:00:41 AM

Ansible Notes

References:

Ansible network modules == https://docs.ansible.com/ansible/latest/modules/list_of_network_modules.html

ansible_network_os == https://github.com/ansible/ansible/blob/devel/docs/docsite/rst/network/user_guide/platform_index.rst

Nested dicts == https://www.jeffgeerling.com/blog/2017/changing-deeply-nested-dict-variable-ansible-playbook

 

CLI Args:

ansible -vvvv -i hosts -u james.bensley -k -a "ls -l" linux_servers

# Ansible CLI args:
#
# -vvvv
# Verbosity level
#
# -i
# Inventory file name, or specify a comma seperated list of hostnames/IPs
#
# -u
# Username for SSH auth to remote device
#
# -k
# Prompt for password for SSH auth to remote device
#
# -c
# How to connect to the remote system (default=smart)
#
# -m
# Ansible module to use (default=command)
#
# -a
# Ansible module args
#
# -e
# Set additional key/value pairs like a network device os (-e "ansible_network_os=iosxr")
#
# Finish with hosts file group or match glob/pattern

 

SSH fingerprints

# Ansible will fail to connect to a network device if the host keys are unknown:
$ansible -i sr1.lab, all -u james.bensley -k -m sros_command -a "commands='show ver'" -c local -e "ansible_network_os=sros"
SSH password:
sr1.lab | FAILED! => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python"
},
"changed": false,
"msg": "Unable to decode JSON from response to exec_command({\"answer\": null, \"command\": \"show ver\", \"prompt\": null}). Received 'None'.",
"rc": 1
}

$ssh sr7.lab
The authenticity of host 'sr1.lab (192.168.0.1)' can't be established.
DSA key fingerprint is SHA256:XXXXXXXXXXXXXXXXX/YYYYYYYYYYYYYYYYYYYYYYYYY.
Are you sure you want to continue connecting (yes/no)?

# To skip host checking (WHICH IS POTENTIALLY INSECURE) set the environment variable ANSIBLE_HOST_KEY_CHECKING to False:
$ANSIBLE_HOST_KEY_CHECKING=False ansible -i sr1.lab, all -u james.bensley -k -m sros_command -a "commands='show ver'" -c local -e "ansible_network_os=sros"
SSH password:
sr1.lab | SUCCESS => {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python"
},
"changed": false,
"stdout": [
"show version \nTiMOS-C-15.0.R7 cpm/hops64 Nokia 7750 SR Copyright (c) 2000-2018 Nokia.\nAll rights reserved. All use subject to applicable license agreements.\nBuilt on Wed Jan 10 15:24:09 PST 2018 by builder in /builds/150B/R7/panos/main"
],
"stdout_lines": [
[
"show version ",
"TiMOS-C-15.0.R7 cpm/hops64 Nokia 7750 SR Copyright (c) 2000-2018 Nokia.",
"All rights reserved. All use subject to applicable license agreements.",
"Built on Wed Jan 10 15:24:09 PST 2018 by builder in /builds/150B/R7/panos/main"
]
]
}

 

Troubleshooting:

# Use -v to increase Ansible verbosity$ansible-playbook -vvvvv -i hosts -u james.bensley -l my_play.yml

# Optionally enable Ansible logging and set the log path:$ANSIBLE_LOG_PATH=./ansible.log ANSIBLE_DISPLAY_ARGS_TO_STDOUT=True ansible-playbook -vvvvv -i hosts -u james.bensley -l my_play.yml
$ less ./ansible.log

 

Start at a specific task:

ansible-play book.yaml --start-at-task "Do that thing"

 

Step through tasks:

ansible-play book.yaml --step

 

Connection methods and jump hosts / bastion hosts:

Connection module: smart/default, ssh, and paramiko

# This will use the default connection method of "smart" which will default to SSH,
# and requires sshpass to be installed, because SSH won't use a user supplied password (-k option)
# without sshpass:
ansible -i hosts -u james.bensley -k linux_servers -a "ls -l"

# To install sshpass on MacOS use:
# xcode-select --install
# brew install https://raw.githubusercontent.com/esolitos/homebrew-ipa/master/Formula/sshpass.rb

# One can explicitly chose ssh using "-c ssh":
ansible -i hosts -u james.bensley -k linux_servers -c ssh -a "ls -l"

# One can use paramiko as a wrapper to SSH, this is helpful if sshpass is unavailable (like on MacOS)
# however, paramiko can't work with jump hosts / bastion hosts:
ansible -i hosts -u james.bensley -k linux_servers -c paramiko -a "ls -l"

# One can set the default connection type for each hosts so that it doesn't need to specified on the CLI:
[linux_servers]
s1.a1.example.com ansible_connection=paramiko
s1.a2.example.com ansible_connection=paramiko

 

Connection module: network_cli

# The network_cli connection type is for device with proprietary SSH CLIs like network devices.
# It doesn't require an SSH key for example:
ansible -i hosts lab --limit r1.lab -u james.bensley -k -m iosxr_facts -c network_cli -e "ansible_network_os=iosxr"
ansible -i ar1.lab, all -u james.bensley -k -m iosxr_command -a "commands='show ver'" -c network_cli -e "ansible_network_os=iosxr"
ansible -i rr0.lab, all -u james.bensley -k -m ios_command -a "commands='show ver'" -c network_cli -e "ansible_network_os=ios"
ansible -i me1.lab, all -u james.bensley -k -m ce_command -a "commands='display ver'" -c network_cli -e "ansible_network_os=ce"
# The following works with 7750s but not with a 7210:
ansible -i sr1.lab, all -u james.bensley -k -m sros_command -a "commands='show ver'" -c network_cli -e "ansible_network_os=sros"
# Set the OS to a Cisco Nexus (like with NAPALM) to get 7210 to work:
ansible -i as1.lab, all -u james.bensley -k -m sros_command -a "commands='show ver'" -c network_cli -e "ansible_network_os=nxos"

 

SSH Jump Hosts:

# A jump host / bastion host will be used automatically if already configured in ~/.ssh/config.
# However, Ansible will not prompt for the password for the jump host, you must already have passwordless SSH keys setup.

# It is also possible to specify an Ansible specific SSH config file by adding the following to ansible.cfg:
[ssh_connection]
ssh_args = -F /path/to/ansible_ssh.cfg

# An SSH config file can contain a specific jump host per remote host.
# However, a specific jump host per remote host can be specified using a variable in the ansible file or on the CLI, if they can't be set in ~/.ssh/config:
# Set the following in host_vars/hostname.yml. To use the same jump host for all hosts set the following in group_vars/all/ansible_ssh.yml:
ansible_ssh_common_args: '-o ProxyCommand="ssh -W %h:%p -q jump_user@jump_host"'

# If the above is set in group_vars for all hotsts, Ansible will try and use the jump host to reach the jump host, so a specific host entry is needed for the jump host which is blank:
host_vars/jump_host/ansible_ssh.yml:
ansible_ssh_common_args: ""

# Via the CLI:
ansible -i hosts -u james.bensley -k linux_servers -e ansible_ssh_common_args='-o ProxyCommand=ssh -W %h:%p -q jump_user@jump_host"' -a "ls -l"

# Another method is to use a port forwarding.
# This forwards port 5555 on the local host to port 22 on the remote device, through the jump_host
ssh -L 5555:device.via.jumphost.only.com:22 jump_username@jump_host
# Then connect to the local host addres on the redirected port, note the ansible variable "ansible_port=5555" which could also be in a host_vars file:
ansible -i 127.0.0.1, all -u james.bensley -k -m iosxr_command -c network_cli -a "commands='show ver'" -e "ansible_network_os=iosxr" -e "ansible_port=5555"

# Sometimes Ansible won't know the host keys of the remote device SSH device, to have Ansible ignore the host key or a changing host key, set the ENV variable "ANSIBLE_HOST_KEY_CHECKING=False" before executing the Ansible command:
ANSIBLE_HOST_KEY_CHECKING=False ansible -i 127.0.0.1, all -u james.bensley -k -m iosxr_command -c network_cli -a "commands='show ver'" -e "ansible_network_os=iosxr" -e "ansible_port=5555"

 

Filters

Combine:

The example below shows how to use the combine() filter to append a new key/value (testb) to an exist dict (vals) or it will update the existing key/value if it exists:

---
- hosts: GROUP_TEST
gather_facts: no
connection: local
tasks:

- name: Set test val A
set_fact:
vals:
testa: 123

- name: Set test val B
set_fact:
vals: "{{ vals|combine({'testb': 789}, recursive=True) }}"
when: inventory_hostname == "host2"

- name: Print before
debug:
msg: "{{ hostvars[inventory_hostname]['vals'] }}"

- name: Clear results variable
set_fact:
vals: null

- name: Print after
debug:
msg: "{{ hostvars[inventory_hostname]['vals'] }}"


$ansible-playbook -i hosts test.yml -v
Using /etc/ansible/ansible.cfg as config file

PLAY [GROUP_TEST] ************************************************************************************************

TASK [Set test val A] **********************************************************************************************
ok: [host1] => {"ansible_facts": {"vals": {"testa": 123}}, "changed": false}
ok: [host2] => {"ansible_facts": {"vals": {"testa": 123}}, "changed": false}

TASK [Set test val B] **********************************************************************************************
ok: [host2] => {"ansible_facts": {"vals": {"testa": 123, "testb": 789}}, "changed": false}
skipping: [host1] => {"changed": false, "skip_reason": "Conditional result was False"}

TASK [Print before] ************************************************************************************************
ok: [host1] => {
"msg": {
"testa": 123
}
}
ok: [host2] => {
"msg": {
"testa": 123,
"testb": 789
}
}

TASK [Clear results variable] **************************************************************************************
ok: [host1] => {"ansible_facts": {"vals": null}, "changed": false}
ok: [host2] => {"ansible_facts": {"vals": null}, "changed": false}

TASK [Print after] *************************************************************************************************
ok: [host1] => {
"msg": ""
}
ok: [host2] => {
"msg": ""
}

 

Random:

Generate random number between 1025 (inclusive) and 65536 (exclusive):

---
- hosts: all
connection: local
gather_facts: no
tasks:
- name: Generate random number
set_fact:
rnd: "{{ 65536 | random(start=1025) }}"

- name: Print random number
debug:
msg: "{{ rnd }}"

$ansible-playbook -i 127.0.0.1, rnd.yml

PLAY [all] **********************************************************************************************************************************************************************************************************************************

TASK [Generate randmo number] ***************************************************************************************************************************************************************************************************************
ok: [127.0.0.1]

TASK [Print random number] ******************************************************************************************************************************************************************************************************************
ok: [127.0.0.1] => {
"msg": "16070"
}

PLAY RECAP **********************************************************************************************************************************************************************************************************************************
127.0.0.1 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

 

Inventory

One can see all goups using ansible-inventory:

$ ansible-inventory --graph | grep "@"
09:34:37
@all:
|--@site_1:
|--@site_2:
|--@site_3:

for site in $(ansible-inventory --graph | grep "site_" | awk -F "@" '{print $2}' | cut -d ":" -f 1)
do
echo "Site: ${site}"
ansible-playbook my_play.yml --limit "${site}"
done

 

Meta Plays

End Play

Handy for debugging, end a play early:

---
- hosts: all
connection: local
gather_facts: no
tasks:

- name: Print text
debug:
msg: "This IS printed"

- name: End play
meta: end_play

- name: Print text
debug:
msg: "This is NOT printed"


$ansible-playbook -v -i 127.0.0.1, end.yml
Using /etc/ansible/ansible.cfg as config file

PLAY [all] **********************************************************************************************************************************************************************************************************************************

TASK [Print text] ***************************************************************************************************************************************************************************************************************************
ok: [127.0.0.1] => {
"msg": "This IS printed"
}

PLAY RECAP **********************************************************************************************************************************************************************************************************************************
127.0.0.1 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

 

Specific Tasks

Brackground Process:

Start a process and fork it to the background, grep for it's PID, then later kill the process by PID:

---
- hosts: all
connection: local
gather_facts: no
tasks:

- name: Start background process
shell: "sleep 25 >/dev/null 2>&1 &"
poll: 0
register: shell_ret

- name: Wait for process to start
pause:
seconds: 2

  - name: Display shell_ret variable
debug:
msg: "{{ shell_ret }}"

- name: Get process PID
shell: pgrep sleep
register: proc_pid

- name: Wait for 10 seconds
pause:
seconds: 10

- name: Kill PID
shell: kill "{{ proc_pid['stdout'] }}"

$ansible-playbook -v -i 127.0.0.1, bg.yml
Using /etc/ansible/ansible.cfg as config file

PLAY [all] **********************************************************************************************************************************************************************************************************************************

TASK [Start background process] *************************************************************************************************************************************************************************************************************
changed: [127.0.0.1] => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python3"}, "changed": true, "cmd": "sleep 25 >/dev/null 2>&1 &", "delta": "0:00:00.002128", "end": "2021-09-17 09:33:17.380410", "rc": 0, "start": "2021-09-17 09:33:17.378282", "stderr": "", "stderr_lines": [], "stdout": "", "stdout_lines": []}

TASK [Wait for process to start] ************************************************************************************************************************************************************************************************************
Pausing for 2 seconds
(ctrl+C then 'C' = continue early, ctrl+C then 'A' = abort)
ok: [127.0.0.1] => {"changed": false, "delta": 2, "echo": true, "rc": 0, "start": "2021-09-17 09:33:17.410033", "stderr": "", "stdout": "Paused for 2.0 seconds", "stop": "2021-09-17 09:33:19.410289", "user_input": ""}

TASK [Display shell_ret variable] ***********************************************************************************************************************************************************************************************************
ok: [127.0.0.1] => {
"msg": {
"ansible_facts": {
"discovered_interpreter_python": "/usr/bin/python3"
},
"changed": true,
"cmd": "sleep 25 >/dev/null 2>&1 &",
"delta": "0:00:00.002128",
"end": "2021-09-17 09:33:17.380410",
"failed": false,
"rc": 0,
"start": "2021-09-17 09:33:17.378282",
"stderr": "",
"stderr_lines": [],
"stdout": "",
"stdout_lines": []
}
}

TASK [Get process PID] **********************************************************************************************************************************************************************************************************************
changed: [127.0.0.1] => {"changed": true, "cmd": "pgrep sleep", "delta": "0:00:00.003304", "end": "2021-09-17 09:33:19.559026", "rc": 0, "start": "2021-09-17 09:33:19.555722", "stderr": "", "stderr_lines": [], "stdout": "371", "stdout_lines": ["371"]}

TASK [Wait for 10 seconds] ******************************************************************************************************************************************************************************************************************
Pausing for 10 seconds
(ctrl+C then 'C' = continue early, ctrl+C then 'A' = abort)
ok: [127.0.0.1] => {"changed": false, "delta": 10, "echo": true, "rc": 0, "start": "2021-09-17 09:33:19.584321", "stderr": "", "stdout": "Paused for 10.0 seconds", "stop": "2021-09-17 09:33:29.584570", "user_input": ""}

TASK [Kill PID] *****************************************************************************************************************************************************************************************************************************
changed: [127.0.0.1] => {"changed": true, "cmd": "kill \"371\"", "delta": "0:00:00.002249", "end": "2021-09-17 09:33:29.720979", "rc": 0, "start": "2021-09-17 09:33:29.718730", "stderr": "", "stderr_lines": [], "stdout": "", "stdout_lines": []}

PLAY RECAP **********************************************************************************************************************************************************************************************************************************
127.0.0.1 : ok=6 changed=3 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

 

ENV Vars:

Read an environment variable, if env_var is zero length, the variable didn't exist or it was empty:

---
- hosts: all
connection: local
gather_facts: no
tasks:

- name: Check if HOME is defined
set_fact:
env_var: "{{ lookup('env','HOME') }}"

- name: End play if zero length
meta: end_play
when: env_var|length == 0

- name: Print the ENV var
debug:
msg: "{{ env_var }}"

# output when checking it $HOME is defined/not-blank

$ansible-playbook -v -i 127.0.0.1, bg.yml
Using /etc/ansible/ansible.cfg as config file

PLAY [all] **********************************************************************************************************************************************************************************************************************************

TASK [Check if HOME is defined] **********************************************************************************************************************************************************************************************************
ok: [127.0.0.1] => {"ansible_facts": {"env_var": "/home/bensley"}, "changed": false}

TASK [Print the ENV var] ********************************************************************************************************************************************************************************************************************
ok: [127.0.0.1] => {
"msg": "/home/bensley"
}

PLAY RECAP **********************************************************************************************************************************************************************************************************************************
127.0.0.1 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0


# Now check if ABC123 variable exists/is not blank

- name: Check if ABC123 is defined
set_fact:
env_var: "{{ lookup('env','ABC123') }}"

$ansible-playbook -v -i 127.0.0.1, bg.yml
Using /etc/ansible/ansible.cfg as config file

PLAY [all] **********************************************************************************************************************************************************************************************************************************

TASK [Check if ABC123 is defined] **********************************************************************************************************************************************************************************************************
ok: [127.0.0.1] => {"ansible_facts": {"env_var": ""}, "changed": false}

PLAY RECAP **********************************************************************************************************************************************************************************************************************************
127.0.0.1 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

 

vars prompt:

---
- hosts: all
connection: local
gather_facts: no

vars_prompt:
- name: userpass
prompt: Enter password
unsafe: yes
private: yes

tasks:

- name: Print result
debug:
msg: "{{ userpass}}"

$ ansible-playbook -i 127.0.0.1, test.yml
Enter password:

PLAY [all] **********************************************************************************************************************************************************************************************************************************

TASK [Print result] *************************************************************************************************************************************************************************************************************************
ok: [127.0.0.1] => {
"msg": "abc123"
}

PLAY RECAP **********************************************************************************************************************************************************************************************************************************
127.0.0.1 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0