'gather_facts' seems to break 'set_fact' and 'hostvars' - ansible

I am using set_fact and hostvars to pass variables between plays within a playbook. My code looks something like this:
- name: Staging play
hosts: localhost
gather_facts: no
vars_prompt:
- name: hostname
prompt: "Enter hostname or group"
private: no
- name: vault
prompt: "Enter vault name"
private: no
- name: input
prompt: "Enter input for role"
private: no
tasks:
- set_fact:
target_host: "{{ hostname }}"
target_vault: "{{ vault }}"
for_role: "{{ input }}"
- name: Execution play
hosts: "{{ hostvars['localhost']['target_host'] }}"
gather_facts: no
vars_files:
- "vault/{{ hostvars['localhost']['target_vault'] }}.yml"
tasks:
- include_role:
name: target_role
vars:
param: "{{ hostvars['localhost']['for_role'] }}"
This arrangement has worked without issue for months. However, our environment has changed and now I need to take a timestamp and pass that to the role as well as the other variable, so I made the following changes (denoted by comments):
- name: Staging play
hosts: localhost
gather_facts: yes # Changed from 'no' to 'yes'
vars_prompt:
- name: hostname
prompt: "Enter hostname or group"
private: no
- name: vault
prompt: "Enter vault name"
private: no
- name: input
prompt: "Enter input for role"
private: no
tasks:
- set_fact:
target_host: "{{ hostname }}"
target_vault: "{{ vault }}"
for_role: "{{ input }}"
current_time: "{{ ansible_date_time.iso8601 }}" # Added fact for current time
- name: Execution play
hosts: "{{ hostvars['localhost']['target_host'] }}"
gather_facts: no
vars_files:
- "vault/{{ hostvars['localhost']['target_vault'] }}.yml"
tasks:
- include_role:
name: target_role
vars:
param: "{{ hostvars['localhost']['for_role'] }}"
timestamp: "{{ hostvars['localhost']['current_time'] # Passed current_time to
Execution Play via hostvars
Now, when I execute, I get the error that the vault hostvars variable is undefined in the Execution Play. After some experimenting, I've found that setting gather_facts: yes in the Staging Play is what is triggering the issue.
However, I need gather_facts enabled in order to use ansible_time_date. I've already verified via debug that the facts are being recorded properly and can be called by hostvars within the Staging Play; just not in the following Execution Play. After hours of research, I can't find any reasoning for why gathering facts in the Staging Play should affect hostvars for the Execution Play or any idea on how to fix it.
At the end of the day, all I need is the current time passed to the included role. Anyone who can come up with a solution that actually works in this use case wins Employee of the Month. Bonus points if you can explain the initial issue with gather_facts.
Thanks!

So, I had to reinvent the wheel a bit, but came up with a much cleaner solution. I simply created a default value for a timestamp in the role itself and added a setup call for date/time at the appropriate point, conditional on there being no existing value for the variable in question.
- name: Gather date and time.
setup:
gather_subset: date_time
when: timestamp is undefined and ansible_date_time is undefined
I was able to leave gather_facts set to no in the dependent playbook but I still have no idea why setting it to yes broke anything in the first place. Any insight in this regard would be appreciated.

... if you can explain the initial issue with gather_facts ... Any insight in this regard would be appreciated.
This is caused by variable precedence and because Ansible do not "overwrite or set a new value" for a variable. So it will depend on when and where they become defined.
You may test with the following example
---
- hosts: localhost
become: false
gather_facts: false
tasks:
- name: Show Gathered Facts
debug:
msg: "{{ hostvars['localhost'].ansible_facts }}" # will be {} only
- name: Gather date and time only
setup:
gather_subset:
- 'date_time'
- '!min'
- name: Show Gathered Facts
debug:
msg: "{{ ansible_facts }}" # from hostvars['localhost'] again
and "try to break it" by adding
- name: Set Fact
set_fact:
ansible_date_time:
date: '1970-01-01'
- name: Show Facts
debug:
msg: "{{ hostvars['localhost'] }}"
Just like to note that for your use case you should use
gather_subset:
- 'date_time'
- '!min'
since your are interested in ansible_date_time only. See what is the exact list of Ansible setup min?.
Be also aware of caching facts since "When created with set_facts’s cacheable option, variables have the high precedence in the play, but are the same as a host facts precedence when they come from the cache."

Related

Getting ansible_play_hosts from previous play?

I have an ansible playbook that interacts with the management card in a bunch of servers, and then produces a report based on that information. Structurally it looks like:
- hosts: all
tasks:
- name: do something with redfish
uri:
...
register: something
- hosts: localhost
tasks:
- name: produce report
template:
...
loop: "{{ SOME_LIST_OF_HOSTS }}"
Originally, the template task in the second was looping over groups.all, but that causes a number of complications if we limited the target hosts using -l on the command line (like ansible-playbook -l only_cluster_a ...). In that case, I would like the template task to loop over only the hosts targeted by the first play. In other words, I want to know ansible_play_hosts_all from the previous play.
This is what I've come up with:
- hosts: all
gather_facts: false
tasks:
- delegate_to: localhost
delegate_facts: true
run_once: true
set_fact:
saved_play_hosts: "{{ ansible_play_hosts_all }}"
...other tasks go here...
- hosts: localhost
gather_facts: false
tasks:
- debug:
msg:
play_hosts: "{{ saved_play_hosts }}"
Is that the best way of doing this?
you could use add_host module: at the end of first play you add a task:
- name: add variables to dummy host
add_host:
name: "variable_holder"
shared_variable: "{{ saved_play_hosts }}"
and you could trap the value in second play:
- name: second play
hosts: localhost
vars:
play_hosts: "{{ hostvars['variable_holder']['shared_variable'] }}"
tasks:
:
:

How to use nested loops with sequence and a list and how get around of curly braces in Ansible

I have a code to backup config using the ios_config module. I used ios_facts to get the hostname of devices and I want to use it to put the backup file in a similarly named folder and also use it in the file name itself.
In the last task of my code, I need to loop through two items - the sequence from 0 to 1(or how many items are in my inventory) as I need to access the hostname in the results and use it in the backup options, and also loop through my inventory of devices which I extracted from a csv file. I am aware of the rule of double curly braces but I do not know how to get around it.
---
- hosts: localhost
gather_facts: false
tasks:
- name: Block
block:
- name: Use CSV
csv_to_facts:
src: '{{playbook_dir}}/NEW/Inventory.csv'
vsheets:
- INFO:
- IP
- OS
- debug:
msg: '{{item.IP}}'
loop: '{{INFO}}'
- name: Create Inventory
add_host:
hostname: '{{item.IP}}'
ansible_network_os: '{{item.OS}}'
ansible_user: cisco
ansible_ssh_pass: cisco
ansible_connection: network_cli
ansible_become: yes
ansible_become_method: enable
groups: group_01
loop: '{{INFO}}'
- name: Gather Facts (IOS)
ios_facts:
register: ios_facts_loop
delegate_to: '{{item}}'
loop: "{{groups['group_01']}}"
- name: Backup Switch (IOS)
ios_config:
backup: yes
backup_options:
dir_path: "tmp/backups/{{ ios_facts_loop.results.{{item[0]}}.ansible_facts.ansible_net_hostname }}"
filename: "{{ios_facts_loop.results.item{{[0]}}.ansible_facts.ansible_net_hostname}} {{ lookup('pipe','date +%Y-%m-%d#%H:%M:%S')}}"
register: backup_ios_location
delegate_to: '{{item[1]}}'
loop:
- with_sequence: "0-{{output|length - 3}}"
- "{{groups['group_01']}}"
TLDR; for vars notation
You cannot add double curly braces inside double curly braces like in your above code. You current var reference:
ios_facts_loop.results.{{item[0]}}.ansible_facts.ansible_net_hostname
should be turned to
ios_facts_loop.results[item[0]].ansible_facts.ansible_net_hostname
# or equivalent
ios_facts_loop.results[item.0].ansible_facts.ansible_net_hostname
Meanwhile, this will only fix your current syntax error (that you didn't share in your question) as the first element in your loop is a string 'with_sequence: "0-X"' which therefore has no index 0.
Attempt to fix the logic
If I understand correctly, for your last task, you just need to loop over the results of your ios_facts register and delegate the task to the server it was taken from. Luckilly, you should already have all the info you need in ios_facts_loop.results
It is a list so you can directly loop over it
Each element should contain an item key with the actual item that was used in the previous run at time of register (i.e. one of your groups['group_01'] element).
So I would try to write your last task like this. Disclaimer this is a pure guess as I didn't see your exact datastructure.
- name: Backup Switch (IOS)
ios_config:
backup: yes
backup_options:
dir_path: "tmp/backups/{{ item.ansible_facts.ansible_net_hostname }}"
filename: "{{ item.ansible_facts.ansible_net_hostname}}{{ lookup('pipe','date +%Y-%m-%d#%H:%M:%S')}}"
register: backup_ios_location
delegate_to: '{{item.item}}'
loop: "{{ ios_facts_loop.results }}"
Going further.
I'm not really familiar with the ios_* modules but they should be really close to other stuff I use daily and I think you could really simplify your playbook taking advantage of more ansible feature (e.g. multiple plays in a playbook). I believe the following should actually do the job:
---
- name: Construct inventory from CSV
hosts: localhost
gather_facts: false
tasks:
- name: Use CSV
csv_to_facts:
src: '{{playbook_dir}}/NEW/Inventory.csv'
vsheets:
- INFO:
- IP
- OS
- name: Create Inventory
add_host:
hostname: '{{item.IP}}'
ansible_network_os: '{{item.OS}}'
ansible_user: cisco
ansible_ssh_pass: cisco
ansible_connection: network_cli
ansible_become: yes
ansible_become_method: enable
groups: group_01
loop: '{{INFO}}'
- name: Backup switches from created inventory
hosts: group_01
gather_facts: false
tasks:
- name: Get facts from network os
ios_facts:
gather_subset: all
- name: Backup Switch (IOS)
ios_config:
backup: yes
backup_options:
dir_path: "tmp/backups/{{ ansible_net_hostname }}"
filename: "{{ ansible_net_hostname }}{{ lookup('pipe','date +%Y-%m-%d#%H:%M:%S') }}"
More background on dot and brackets notation for vars
You can basically navigate a yaml datastructure with two notation which are equivalent.
the dot notation
a_list_var.index_number
a_hasmap_var.keyname
the brackets notation
a_list_var[index_number]
a_hashmap_var['key_name']
If we take the following example:
my_servers:
hostA:
ips:
- x.x.x.x
- y.y.y.y
env:
shell: bash
home: somewhere
hostB:
ips:
- a.a.a.a
- b.b.b.b
env:
shell: sh
home: elsewhere
The following notation are all strictly equivalent:
# all vars of hostA
hostA_vars: "{{ my_servers.hostA }}"
hostA_vars: "{{ my_server['hostA'] }}"
# first IP of hostB
hostB_ip: "{{ my_servers.hostB.0 }}"
hostB_ip: "{{ my_servers.hostB[0] }}"
hostB_ip: "{{ my_servers['hostB'].0 }}"
hostB_ip: "{{ my_servers['hostB'][0] }}"
As you can see, the dot notation tends to be less verbose and more readable. Meanwhile, you cannot use a variable identifier with the dot notation. So If you want to ave the home env of a variable server you would have to use:
# set a var for server
server: hostA
# all equivalent again
server_home: "{{ my_servers[server].env.home }}"
server_home: "{{ my_servers[server]['env'].home }}"
server_home: "{{ my_servers[server].env['home'] }}"
server_home: "{{ my_servers[server]['env']['home'] }}"

In ansible variable for hosts from vars_prompt no longer accepted [duplicate]

I want to write a bootstrapper playbook for new machines in Ansible which will reconfigure the network settings. At the time of the first execution target machines will have DHCP-assigned address.
The user who is supposed to execute the playbook knows the assigned IP address of a new machine. I would like to prompt the user for is value.
vars_prompt module allows getting input from the user, however it is defined under hosts section effectively preventing host address as the required value.
Is it possible without using a wrapper script modifying inventory file?
The right way to do this is to create a dynamic host with add_host and place it in a new group, then start a new play that targets that group. That way, if you have other connection vars that need to be set ahead of time (credentials/keys/etc) you could set them on an empty group in inventory, then add the host to it dynamically. For example:
- hosts: localhost
gather_facts: no
vars_prompt:
- name: target_host
prompt: please enter the target host IP
private: no
tasks:
- add_host:
name: "{{ target_host }}"
groups: dynamically_created_hosts
- hosts: dynamically_created_hosts
tasks:
- debug: msg="do things on target host here"
You could pass it with extra-vars instead.
Simply make your hosts section a variable such as {{ hosts_prompt }} and then pass the host on the command line like so:
ansible-playbook -i inventory/environment playbook.yml --extra-vars "hosts_prompt=192.168.1.10"
Or if you are using the default inventory file location of /etc/ansible/hosts you could simply use:
ansible-playbook playbook.yml --extra-vars "hosts_prompt=192.168.1.10"
Adding to Matt's answer for multiple hosts.
input example would be 192.0.2.10,192.0.2.11
- hosts: localhost
gather_facts: no
vars_prompt:
- name: target_host
prompt: please enter the target host IP
private: no
tasks:
- add_host:
name: "{{ item }}"
groups: dynamically_created_hosts
with_items: "{{ target_host.split(',') }}"
- hosts: dynamically_created_hosts
tasks:
- debug: msg="do things on target host here"
Disclaimer: The accepted answer offers the best solution to the problem. While this one is working it is based on a hack and I leave it as a reference.
I found out it was possible use a currently undocumented hack (credit to Bruce P for pointing me to the post) that turns the value of -i / --inventory parameter into an ad hoc list of hosts (reference). With just the hostname/ip address and a trailing space (like below) it refers to a single host without the need for the inventory file to exist.
Command:
ansible-playbook -i "192.168.1.21," playbook.yml
And accordingly playbook.yml can be run against all hosts (which in the above example will be equal to a single host 192.168.1.21):
- hosts: all
The list might contain more than one ip address -i "192.168.1.21,192.168.1.22"
Adding to Jacob's and Matt's examples, with the inclusion of a username and password prompt:
---
- hosts: localhost
pre_tasks:
- name: verify_ansible_version
assert:
that: "ansible_version.full is version_compare('2.10.7', '>=')"
msg: "Error: You must update Ansible to at least version 2.10.7 to run this playbook..."
vars_prompt:
- name: target_hosts
prompt: |
Enter Target Host IP[s] or Hostname[s] (comma separated)
(example: 1.1.1.1,myhost.example.com)
private: false
- name: username
prompt: Enter Target Host[s] Login Username
private: false
- name: password
prompt: Enter Target Host[s] Login Password
private: true
tasks:
- add_host:
name: "{{ item }}"
groups: host_groups
with_items:
- "{{ target_hosts.split(',') }}"
- add_host:
name: login
username: "{{ username }}"
password: "{{ password }}"
- hosts: host_groups
remote_user: "{{ hostvars['login']['username'] }}"
vars:
ansible_password: "{{ hostvars['login']['password'] }}"
ansible_become: yes
ansible_become_method: sudo
ansible_become_pass: "{{ hostvars['login']['password'] }}"
roles:
- my_role

Using a variable as a default value in vars_prompt in Ansible

I was trying to use vars_prompt in Ansible with default values taken from facts (or otherwise a previously defined variable). The playbook is intended be used as an ad-hoc one for initial provisioning.
My playbook:
---
- hosts: server01
gather_facts: True
vars_prompt:
- name: new_hostname
prompt: please enter the name for the target
default: "{{ ansible_hostname }}"
private: no
tasks:
- debug: msg="{{ new_hostname }}"
Current result:
please enter the name for the target [{{ ansible_hostname }}]:
ERROR! 'ansible_hostname' is undefined
Expected results (assuming ansible_hostname=server01:
please enter the name for the target [server01]:
Is it possible to achieve in Ansible?
This can be implemented using the pause module:
---
- hosts: server01
gather_facts: True
tasks:
- pause:
prompt: please enter the name for the target [{{ ansible_hostname }}]
register: prompt
- debug:
msg: "{{ prompt.user_input if prompt.user_input else ansible_hostname }}"

How to prompt user for a target host in Ansible?

I want to write a bootstrapper playbook for new machines in Ansible which will reconfigure the network settings. At the time of the first execution target machines will have DHCP-assigned address.
The user who is supposed to execute the playbook knows the assigned IP address of a new machine. I would like to prompt the user for is value.
vars_prompt module allows getting input from the user, however it is defined under hosts section effectively preventing host address as the required value.
Is it possible without using a wrapper script modifying inventory file?
The right way to do this is to create a dynamic host with add_host and place it in a new group, then start a new play that targets that group. That way, if you have other connection vars that need to be set ahead of time (credentials/keys/etc) you could set them on an empty group in inventory, then add the host to it dynamically. For example:
- hosts: localhost
gather_facts: no
vars_prompt:
- name: target_host
prompt: please enter the target host IP
private: no
tasks:
- add_host:
name: "{{ target_host }}"
groups: dynamically_created_hosts
- hosts: dynamically_created_hosts
tasks:
- debug: msg="do things on target host here"
You could pass it with extra-vars instead.
Simply make your hosts section a variable such as {{ hosts_prompt }} and then pass the host on the command line like so:
ansible-playbook -i inventory/environment playbook.yml --extra-vars "hosts_prompt=192.168.1.10"
Or if you are using the default inventory file location of /etc/ansible/hosts you could simply use:
ansible-playbook playbook.yml --extra-vars "hosts_prompt=192.168.1.10"
Adding to Matt's answer for multiple hosts.
input example would be 192.0.2.10,192.0.2.11
- hosts: localhost
gather_facts: no
vars_prompt:
- name: target_host
prompt: please enter the target host IP
private: no
tasks:
- add_host:
name: "{{ item }}"
groups: dynamically_created_hosts
with_items: "{{ target_host.split(',') }}"
- hosts: dynamically_created_hosts
tasks:
- debug: msg="do things on target host here"
Disclaimer: The accepted answer offers the best solution to the problem. While this one is working it is based on a hack and I leave it as a reference.
I found out it was possible use a currently undocumented hack (credit to Bruce P for pointing me to the post) that turns the value of -i / --inventory parameter into an ad hoc list of hosts (reference). With just the hostname/ip address and a trailing space (like below) it refers to a single host without the need for the inventory file to exist.
Command:
ansible-playbook -i "192.168.1.21," playbook.yml
And accordingly playbook.yml can be run against all hosts (which in the above example will be equal to a single host 192.168.1.21):
- hosts: all
The list might contain more than one ip address -i "192.168.1.21,192.168.1.22"
Adding to Jacob's and Matt's examples, with the inclusion of a username and password prompt:
---
- hosts: localhost
pre_tasks:
- name: verify_ansible_version
assert:
that: "ansible_version.full is version_compare('2.10.7', '>=')"
msg: "Error: You must update Ansible to at least version 2.10.7 to run this playbook..."
vars_prompt:
- name: target_hosts
prompt: |
Enter Target Host IP[s] or Hostname[s] (comma separated)
(example: 1.1.1.1,myhost.example.com)
private: false
- name: username
prompt: Enter Target Host[s] Login Username
private: false
- name: password
prompt: Enter Target Host[s] Login Password
private: true
tasks:
- add_host:
name: "{{ item }}"
groups: host_groups
with_items:
- "{{ target_hosts.split(',') }}"
- add_host:
name: login
username: "{{ username }}"
password: "{{ password }}"
- hosts: host_groups
remote_user: "{{ hostvars['login']['username'] }}"
vars:
ansible_password: "{{ hostvars['login']['password'] }}"
ansible_become: yes
ansible_become_method: sudo
ansible_become_pass: "{{ hostvars['login']['password'] }}"
roles:
- my_role

Resources