Using conditionals in a Playbook to execute based on IP address - ansible

I have to change the windows computer name on 40 machines in AWS. I tried using the gather_facts to set a condition to only execute if the ip matches. But for some reason it doesn't pick it up. My solution so far to the problem(highly inefficient is to make individual host groups one per ip. I know there has to be a better way to go about this any input would be appreciated.
This is what I have that works
---
- hosts: windows_machine1
gather_facts: yes
tasks:
- name: Change the hostname to newname1
win_hostname:
name: newname1
register: res
- hosts: windows_machine2
tasks:
- name: Change the hostname to newname2
win_hostname:
name: newname2
register: res
- name: Reboot
win_reboot:
when: res.reboot_required
I tried two ways to make the conditional both resulted in a error when ran.
---
- hosts: windows_machine1
gather_facts: yes
tasks:
- name: Change the hostname to newname1
win_hostname:
name: newname1
register: res
when: ansible_facts['ansible_all_ipv4_addresses'] == '10.x.x.x
- name: Change the hostname to newname2
win_hostname:
name: newname2
register: res
when: ansible_facts['address'] == '10.x.x.x'
- name: Reboot
win_reboot:
when: res.reboot_required
It will fail saying the conditional check failed. Because my conditionals are bad. Does anyone know how to make a conditional based on ip?

Disclaimer: I only run Ansible on Linux hosts, so I guess there is a chance things are different on Windows hosts.
You don't specify ansible_facts, and instead just start at the specific root fact.
In the first case, the fact you are trying to get to is not going to help you, because it returns a list of all the IP's on the system. Even if there is only one, it still returns a list, which you can't then simply make a string comparison against.
This should do what you want in the first instance:
- name: Change the hostname to newname2
win_hostname:
name: newname2
register: res
when: "ansible_default_ipv4.address == '10.0.0.1'"
Are you intending to duplicate this block of code, one per host? If so consider setting a variable to lookup IP and new name:
- hosts: all
vars:
ip_newname:
10.0.0.1: newname1
10.0.0.2: newname2
10.0.0.3: newname3
tasks:
- name: Change the hostname
win_hostname:
name: "{{ ip_newname[ansible_default_ipv4.address] }}"
register: res
when: ansible_default_ipv4.address in ip_newname.keys()
- name: Reboot
win_reboot:
when: res is defined and res.reboot_required

Related

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

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."

Ansible inventory, make ansible_host point to two private iP addresses

wanted to know what syntax there is for making ansible_host point to two different addresses
[my_service]
dev_my_service_01 ansible_host=10.10.10.10
or
dev_my_service_01 ansible_host=10.10.10.10 20.20.20.20
or
dev_my_service_01 ansible_host=10.10.10.10/20.20.20.20
to make it hit either this or this address.
It sounds like what you want is to conditionally define dev_my_service_01, which can be done in one of two ways:
add two inventory entries, for dev_my_service_01a and dev_my_service_01b, and just accept that at least one play will fail
or start out with only localhost in the play, then use add_host: as localhost learns which of the two IPs is the "real" one, kind of like the snippet below
- hosts: localhost
gather_facts: no
tasks:
- command: ping -c 1 10.10.10.10
ignore_errors: yes
register: is_10
- # etc etc
- add_host:
name: '{{ "10.10.10.10" if (is_10 is success) else "20.20.20.20" }}'
groups:
- my_service
- hosts: my_service
tasks:
- debug:
msg: hello from the discovered host {{ ansible_host }}

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'] }}"

vars_prompt not working in role - Ansible

I'm trying to automate hostname creation for 10x machines using ansible roles. What I want when executing playbook, as this wait for enter the user name manually.
I tried with vars_prompt module for satisfying the requirement. But here for a single *.yml file, I'm able to see the expected results
Without role - I can see the input is taken to variable host. but this works fine.
#host.yml .
---
- hosts: ubuntu
user: test
sudo: yes
vars_prompt:
- name: host
prompt: "Specify host name?"
private: no
tasks:
- debug:
msg: ' log as {{ host }}'
- name: Changing hostname
hostname:
name: '{{ host }}'
With role, < this is not working > { vars_prompt not working }
#role.yml
---
- hosts: ubuntu
user: test
sudo: yes
roles:
# - hostname
#hostname/tasks/main.yml
- name: testing prompt
vars_prompt:
- name: host
prompt: "Specify host name?"
- name: Ansible prompt example
debug:
msg: "{{ host }}"
#Changing hostname
- name: Changing hostname
hostname:
name: "{{ host }}"
Here I'm getting error as
ERROR! no action detected in task. This often indicates a misspelled module name, or incorrect module path.
The error appears to have been in '/root/roles/hostname/tasks/main.yml': line 1, column 3, but may
be elsewhere in the file depending on the exact syntax problem.
The offending line appears to be:
- name: testing prompt
^ here
My expectation is to set some parameters need to set manually as input while the execution of playbook. Need to work this vars_prompt module in role.
Run 1st play with serial: 1 and enter the hostnames. 2nd play (in the same playbook) will use the facts.
- hosts: ubuntu
serial: 1
user: test
sudo: yes
tasks:
- pause:
prompt: "Specify hostname for {{ inventory_hostname }}?"
echo: yes
register: result
- set_fact:
host: "{{ result.user_input }}"
- hosts: ubuntu
user: test
sudo: yes
roles:
- hostname

How to avoid a playbook to run when no host matches?

Use case: users can provide a host name and will trigger a playbook run. In case the hostname has a typo I want to fail complete playbook run when "no hosts matched". I want to fail it since I like to detect a failure im Bamboo (which I use for CD/CI) to run the playbook.
I have done quite extensive research. It seems that it is a wanted behavior that the playbook exists with an exit code = 0 when no host matches. Here is one indication I found. I agree that the general behavior should be like this.
So I need for my use case an extra check. I tried the following:
- name: Deploy product
hosts: "{{ target_hosts }}"
gather_facts: no
any_errors_fatal: true
pre_tasks:
- name: Check for a valid target host
fail:
msg: "The provided host is not knwon"
when: target_hosts not in groups.tomcat_servers
But since there is no host match the playbook will not run, that is ok but it also ends with exit code 0. That way I can not fail the run in my automation system (Bamboo).
Due to this I am looking for a solution to throw an exit code != 0 when no host matches.
Add a play which would set a fact if a host matched, then check that fact in a second play:
- name: Check hosts
hosts: "{{ target_hosts }}"
gather_facts: no
tasks:
- set_fact:
hosts_confirmed: true
delegate_to: localhost
delegate_facts: true
- name: Verify hosts
hosts: localhost
gather_facts: no
tasks:
- assert:
that: hosts_confirmed | default(false)
- name: The real play
hosts: "{{ target_hosts }}"
# ...

Resources