How to use host_vars in Ansible? - ansible

I created a variable in group_vars
group_vars/all
---
hostname: name1
I want to set name2 to the real host(10.20.30.40), so I created a file and set the hostname again there
host_vars/10.20.30.40
---
hostname: name2
When I run the playbook, it returned name1 but not name2:
roles/os/tasks/main.yml
- name: Print hostname
ansible.builtin.debug:
msg: "{{ hostname }}"
Result:
TASK [server : Print hostname] *************************************
ok: [web_server] => {
"msg": "name1"
}
I want to set the variable in each host that I want to update, isn't it the right usage?
And, if I name the host file this way under the host_vars folder, does it work?
web_server
The inventory:
[web_server]
10.20.30.40
playbook:
---
- hosts: web_server
become: true
vars_files:
- group_vars/all
roles:
- os

Q: "When I run the playbook, it returned name1 but not name2."
shell> cat hosts
[web_server]
10.20.30.40
shell> cat group_vars/all
---
hostname: name1
shell> cat host_vars/10.20.30.40
---
hostname: name2
shell> cat playbook.yml
---
- hosts: web_server
vars_files:
- group_vars/all
tasks:
- debug:
var: hostname
A: Don't include group_vars/all in a playbook.
The playbook's group_vars/all are included by the playbook automatically at precedence 5. See Understanding variable precedence. If you put group_vars/all into the vars_files (precedence 14) you override the host_vars (precedence 10).
Example. Given the inventory
shell> cat hosts
[web_server]
10.20.30.40
10.20.30.41
the playbook
shell> cat playbook.yml
---
- hosts: web_server
gather_facts: false
tasks:
- debug:
var: hostname
gives as expected
shell> ansible-playbook -i hosts playbook.yml
PLAY [web_server] *********************************************************
TASK [debug] **************************************************************
ok: [10.20.30.40] =>
hostname: name2
ok: [10.20.30.41] =>
hostname: name1
The variable in host_vars/10.20.30.40 override the variable in group_vars/all

Related

Ansible - How to build dynamic inventory group from inventory host file

Below is sample inventory host file:
cat inventory.hosts
[dev_flipkart_app]
myhost6 ansible_user=fappuser
myhost9 ansible_user=fappuser
[dev_amazon_app]
myhost1 ansible_user=aappuser
[dev_snapdeal_app]
myhost4 ansible_user=sappuser
Below is my playbook main.yml
---
- name: Playbook
hosts: "{{ ENV }}_{{ appname }}_app"
tasks:
- name: ensure apache is at the latest version
yum:
name: httpd
state: latest
This works fine when single {{ appname }} is passed, but how to i build a dynamic inventory from inventory.hosts using add_host when multiple {{ appname }} is passed?
ansible-playbook -i inventory.hosts main.yml -e ENV=dev -e appname="flipkart,amazon"
Expected dynamic host group should be as below:
myhost6 ansible_user=fappuser
myhost9 ansible_user=fappuser
myhost1 ansible_user=aappuser
Kindly suggest.
You seem to have noticed that hosts: can be a jinja2 expression, but then stopped short of realizing that hosts: can be a jinja2 expression
- hosts: "{{ appname | split(',') | map('regex_replace', '(.+)', ENV ~ '_\\1_app') }}"
tasks:
- debug: var=ansible_user
when run with your cited example command line
ansible-playbook -i inventory.hosts main.yml -e ENV=dev -e appname="flipkart,amazon"
produces
ok: [myhost6] => {
"ansible_user": "fappuser"
}
ok: [myhost9] => {
"ansible_user": "fappuser"
}
ok: [myhost1] => {
"ansible_user": "aappuser"
}
For example, the playbook
- hosts: "{{ [ENV]|product(appname.split(','))|product(['app'])|
map('flatten')|map('join', '_') }}"
tasks:
- debug: var=ansible_user
gives (abridged)
shell> ansible-playbook pb.yml -e ENV=dev -e appname="flipkart,amazon"
...
ok: [myhost6] =>
ansible_user: fappuser
ok: [myhost9] =>
ansible_user: fappuser
ok: [myhost1] =>
ansible_user: aappuser
The next option is to compose a group. See
shell> ansible-doc -t inventory constructed
Create the inventory files
shell> cat hosts/01-hosts
[dev_flipkart_app]
myhost6 ansible_user=fappuser
myhost9 ansible_user=fappuser
[dev_amazon_app]
myhost1 ansible_user=aappuser
[dev_snapdeal_app]
myhost4 ansible_user=sappuser
shell> cat hosts/02-constructed.yml
plugin: constructed
strict: True
use_extra_vars: True
compose:
my_dev_regex: ENV
my_appname_regex: appname.split(',')|join('|')
my_groups: group_names
my_composed_group: group_names|
select('search', ENV)|
select('search', appname.split(',')|join('|'))
groups:
my_composed_group: my_groups|
select('search', my_dev_regex)|
select('search', my_appname_regex)|
length > 0
and enable the constructed plugin in the configuration. For example,
shell> grep constructed ansible.cfg
enable_plugins = ini, constructed
Test the inventory. For example,
shell> ansible-inventory -e ENV=dev -e appname="flipkart,amazon" --list --yaml
all:
children:
dev_amazon_app:
hosts:
myhost1:
ENV: dev
ansible_user: aappuser
appname: flipkart,amazon
my_appname_regex: flipkart|amazon
my_composed_group:
- dev_amazon_app
my_dev_regex: dev
my_groups:
- dev_amazon_app
dev_flipkart_app:
hosts:
myhost6:
ENV: dev
ansible_user: fappuser
appname: flipkart,amazon
my_appname_regex: flipkart|amazon
my_composed_group:
- dev_flipkart_app
my_dev_regex: dev
my_groups:
- dev_flipkart_app
myhost9:
ENV: dev
ansible_user: fappuser
appname: flipkart,amazon
my_appname_regex: flipkart|amazon
my_composed_group:
- dev_flipkart_app
my_dev_regex: dev
my_groups:
- dev_flipkart_app
dev_snapdeal_app:
hosts:
myhost4:
ENV: dev
ansible_user: sappuser
appname: flipkart,amazon
my_appname_regex: flipkart|amazon
my_composed_group: []
my_dev_regex: dev
my_groups:
- dev_snapdeal_app
my_composed_group:
hosts:
myhost1: {}
myhost6: {}
myhost9: {}
ungrouped: {}
Create a playbook and use the group my_composed_group
- hosts: my_composed_group
tasks:
- debug: var=ansible_user
This playbook gives the same results
shell> ansible-playbook pb.yml -e ENV=dev -e appname="flipkart,amazon"
...
ok: [myhost6] =>
ansible_user: fappuser
ok: [myhost9] =>
ansible_user: fappuser
ok: [myhost1] =>
ansible_user: aappuser

Ansible playbook doesnt get values from inventory group_vars

I have got a simple playbook:
- hosts: all
serial: 1
order: "{{ run_order }}"
gather_facts: false
tasks:
- ping:
register: resultt
My inventory tree:
.
├── group_vars
│   └── g1
│   └── values.yml
├── host_vars
└── inv
inv file:
[g1:children]
m1
m2
[g2:children]
m3
m4
[m1]
192.168.0.60
[m2]
192.168.0.61
[m3]
192.168.0.62
[m4]
192.168.0.63
group_vars/g1/values.yml:
---
run_order: 'reverse_sorted'
When I try to run playbook:
ansible-playbook -i inv-test/inv --limit='g1' my-playbook.yml
I get the error:
ERROR! The field 'order' has an invalid value, which includes an undefined variable. The error was: 'run_order' is undefined
The error appears to be in '/home/mk/throttle': line 1, column 3, but may
be elsewhere in the file depending on the exact syntax problem.
The offending line appears to be:
- hosts: all
^ here
Although the output from ansible-inventory -i inv --graph --vars is correct
Why ansible doesn't parse vars from inventory groups_vars?
Ansible version 2.10.15
The problem is that you want to use a host variable in the scope of a play. See Scoping variables. It's possible if you create such a host variable first. Then you can use hostvars to reference the host variable. For example, given the simplified inventory
shell> cat hosts
[g1]
host_01
host_02
and the group_vars
shell> cat group_vars/g1.yml
run_order: 'reverse_sorted'
The playbook
shell> cat pb.yml
- hosts: all
gather_facts: false
tasks:
- debug:
var: run_order
gives (abridged)
shell> ansible-playbook -i hosts pb.yml
ok: [host_01] =>
run_order: reverse_sorted
ok: [host_02] =>
run_order: reverse_sorted
But, the playbook below
shell> cat pb.yml
- hosts: all
gather_facts: false
order: "{{ run_order }}"
tasks:
- debug:
var: run_order
fails because the variable run_order is not defined in the scope of the play
ERROR! The field 'order' has an invalid value, which includes an undefined variable. The error was: 'run_order' is undefined
The (awkward) solution is to create the hostvars in the first play and use it in the second play. For example, the second play in the playbook below is executed in the reverse order
shell> cat pb.yml
- hosts: all
gather_facts: false
tasks:
- debug:
var: run_order
- hosts: all
gather_facts: false
serial: 1
order: "{{ hostvars.host_01.run_order }}"
tasks:
- debug:
var: run_order
gives
shell> ansible-playbook -i hosts pb.yml
PLAY [all] ***********************************************************************************
TASK [debug] *********************************************************************************
ok: [host_01] =>
run_order: reverse_sorted
ok: [host_02] =>
run_order: reverse_sorted
PLAY [all] ***********************************************************************************
TASK [debug] *********************************************************************************
ok: [host_02] =>
run_order: reverse_sorted
PLAY [all] ***********************************************************************************
TASK [debug] *********************************************************************************
ok: [host_01] =>
run_order: reverse_sorted

How to pass additional environment variables to the imported ansible playbook

I have a main_play.yml Ansible playbook in which I am importing a reusable playbook a.yml.
main_play.yml
- import_playbook: "reusable_playbooks/a.yml"
a.yml
---
- name: my_playbook
hosts: "{{ HOSTS }}"
force_handlers: true
gather_facts: false
environment:
APP_DEFAULT_PORT: "{{ APP_DEFAULT_PORT }}"
tasks:
- name: Print Msg
debug:
msg: "hello"
My question is: how can I pass an additional environment variable from my main_playbook.yml playbook to my re-usable playbook a.yml (if needed) so that the environment variables become like
environment:
APP_DEFAULT_PORT: "{{ APP_DEFAULT_PORT }}"
SPRING_PROFILE: "{{ SPRING_PROFILE }}"
import_playbook is not really a module but a core feature. It does not allow for any parameter to be passed to the imported playbook. You can see this keyword as a simple commodity to facilitate playing several playbooks in a row exactly as if they were defined in the same file.
So your problem comes down to:
How do I pass additional environment variables to a play ?
Here is one solution with illustrations to use it with extra_vars or setting a fact from a previous play. This far from being exhaustive but I hope it will guide you to you own best solution.
To ease readability:
I used the APP_ prefix for all environment variables in my below examples and filtered only on those for the results.
I truncated the playbook output to the only relevant debug task
We can define the following reusable.yml playbook containing a single play
---
- hosts: localhost
gather_facts: false
vars:
default_env:
APP_DEFAULT_PORT: "{{ APP_DEFAULT_PORT | d(8080) }}"
environment: "{{ default_env | combine(additionnal_env | d({})) }}"
tasks:
- name: get the output on env for APP_* vars
shell: env | grep -i app_
register: env_cmd
changed_when: false
- name: debug the output of env
debug:
var: env_cmd.stdout_lines
We can directly run this playbook as-is which will give
$ ansible-playbook reusable.yml
[... truncated ...]
TASK [debug the output of env] ************************************************************************************************************************************************************************************
ok: [localhost] => {
"env_cmd.stdout_lines": [
"APP_DEFAULT_PORT=8080"
]
}
We can override the default port with
$ ansible-playbook reusable.yml -e APP_DEFAULT_PORT=1234
[... truncated ...]
TASK [debug the output of env] ************************************************************************************************************************************************************************************
ok: [localhost] => {
"env_cmd.stdout_lines": [
"APP_DEFAULT_PORT=1234"
]
}
We can pass additional environment variables with:
$ ansible-playbook reusable.yml -e '{"additionnal_env":{"APP_SPRING_PROFILE": "/toto/pipo"}}'
[... truncated ...]
TASK [debug the output of env] ************************************************************************************************************************************************************************************
ok: [localhost] => {
"env_cmd.stdout_lines": [
"APP_SPRING_PROFILE=/toto/pipo",
"APP_DEFAULT_PORT=8080"
]
}
Now if we want to do this from a parent playbook, we can set the needed variable for the given host in a previous play. We can define a parent.yml playbook:
---
- hosts: localhost
gather_facts: false
tasks:
- name: define additionnal env vars for this host to be used in next play(s)
set_fact:
additionnal_env:
APP_WHATEVER: some_value
APP_VERY_IMPORTANT: "ho yes!"
- import_playbook: reusable.yml
which will give:
$ ansible-playbook parent.yml
[... truncated ...]
TASK [define additionnal env vars for this host to be used in next play(s)] ************************************************************************************************************************
ok: [localhost]
[... truncated ...]
TASK [debug the output of env] ************************************************************************************************************************************************************************************
ok: [localhost] => {
"env_cmd.stdout_lines": [
"APP_WHATEVER=some_value",
"APP_VERY_IMPORTANT=ho yes!",
"APP_DEFAULT_PORT=8080"
]
}

How to use ansible-playbook --limit with an IP address, rather than a hostname?

Our inventory in INI style looks like this:
foo-host ansible_host=1.2.3.4 some_var=bla
bar-host ansible_host=5.6.7.8 some_var=blup
I can limit a playbook run to a single host by using the host alias:
$ ansible-playbook playbook.yml --limit foo-host
But I can't limit the run by mentioning the host's IP address from the ansible_host variable:
$ ansible-playbook playbook.yml --limit 1.2.3.4
ERROR! Specified hosts and/or --limit does not match any hosts
The reason I want to do that is because Ansible is triggered by an external system that only knows the IP address, but not the alias.
Is there a way to make this work? Mangling the IP address (e.g. ip_1_2_3_4) would be acceptable.
Things I've considered:
Turn it on its head and identify all hosts by IP address:
1.2.3.4 some_var=bla
5.6.7.8 some_var=blup
But now we can't use the nice host aliases anymore, and the inventory file is less readable too.
Write a custom inventory script that is run after the regular inventory, and creates a group like ip_1_2_3_4 containing only that single host, so we can use --limit ip_1_2_3_4. But there's no way to access previously loaded inventory from inventory scripts, so I don't know which groups to create.
Create the new groups dynamically using the group_by module. But because this is a task, it is run only after --limit has already decided that there are no hosts matching the pattern, and at that point Ansible just gives up and doesn't run the group_by task anymore.
Better solutions still welcome, but currently I'm doing it with a small inventory plugin, which (as opposed to an inventory script) does have access to previously added inventory:
plugins/inventory/ip_based_groups.py
import os.path
import re
from ansible.plugins.inventory import BaseInventoryPlugin
from ansible.inventory.group import Group
PATH_PLACEHOLDER = 'IP_BASED_GROUPS'
IP_RE = re.compile('^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')
class InventoryModule(BaseInventoryPlugin):
'''
This inventory plugin does not create any hosts, but just adds groups based
on IP addresses. For each host whose ansible_host looks like an IPv4
address (e.g. 1.2.3.4), a corresponding group is created by prefixing the
IP address with 'ip_' and replacing dots by underscores (e.g. ip_1_2_3_4).
Use it by putting the literal string IP_BASED_GROUPS at the end of the list
of inventory sources.
'''
NAME = 'ip_based_groups'
def verify_file(self, path):
return self._is_path_placeholder(path)
def parse(self, inventory, loader, path, cache=True):
if not self._is_path_placeholder(path):
return
for host_name, host in inventory.hosts.items():
ansible_host = host.vars.get('ansible_host', '')
if self._is_ip_address(ansible_host):
group = 'ip_' + ansible_host.replace('.', '_')
inventory.add_group(group)
inventory.add_host(host_name, group)
def _is_path_placeholder(self, path):
return os.path.basename(path) == PATH_PLACEHOLDER
def _is_ip_address(self, s):
return bool(IP_RE.match(s))
ansible.cfg
[defaults]
# Load plugins from these directories.
inventory_plugins = plugins/inventory
# Directory that contains all inventory files, and placeholder to create
# IP-based groups.
inventory = inventory/,IP_BASED_GROUPS
[inventory]
# Enable our custom inventory plugin.
enable_plugins = ip_based_groups, host_list, script, auto, yaml, ini, toml
Q: Playbook running a script running a playbook... it would work, but it's a bit hacky
A: FWIW. It's possible to use json_query and avoid the script. For example
- hosts: all
gather_facts: false
tasks:
- set_fact:
my_host: "{{ hostvars|dict2items|json_query(query)|first }}"
vars:
query: "[?value.ansible_host == '{{ my_host_ip }}' ].key"
run_once: true
- add_host:
hostname: "{{ my_host }}"
groups: my_group
run_once: true
- hosts: my_group
gather_facts: false
tasks:
- debug:
var: inventory_hostname
Q: "Unfortunately I'm running ansible-playbook from AWX, so no wrapper scripts allowed."
A: It is possible to run the script from the playbook. For example the playbook below
- hosts: all
gather_facts: false
tasks:
- set_fact:
my_host: "{{ lookup('pipe', playbook_dir ~ '/script.sh ' ~ my_host_ip) }}"
delegate_to: localhost
run_once: true
- add_host:
hostname: "{{ my_host }}"
groups: my_group
run_once: true
- hosts: my_group
gather_facts: false
tasks:
- debug:
var: inventory_hostname
gives
$ ansible-playbook -e 'my_host_ip=10.1.0.53' play.yml
PLAY [all] ********************
TASK [set_fact] ***************
ok: [test_01 -> localhost]
TASK [add_host] ***************
changed: [test_01]
PLAY [my_group] ***************
TASK [debug] ************
ok: [test_03] => {
"inventory_hostname": "test_03"
}
(Fit the script to print the hostname only.)
Q: I can limit a playbook run to a single host by using the host alias:
$ ansible-playbook playbook.yml --limit foo-host
But I can't limit the run by mentioning the host's IP address from the ansible_host variable
$ ansible-playbook playbook.yml --limit 1.2.3.4
A: ansible-inventory and jq are able to resolve the host. For example the script
#!/bin/bash
my_host="$(ansible-inventory --list | jq '._meta.hostvars | to_entries[] | select (.value.ansible_host=="'"$1"'") | .key')"
my_host="$(echo $my_host | sed -e 's/^"//' -e 's/"$//')"
echo host: $my_host
ansible-playbook -l $my_host play.yml
with the inventory
test_01 ansible_host=10.1.0.51
test_02 ansible_host=10.1.0.52
test_03 ansible_host=10.1.0.53
and with the playbook.yml
- hosts: all
gather_facts: false
tasks:
- debug:
var: inventory_hostname
gives
$ ./script.bash 10.1.0.53
host: test_03
PLAY [all] **********
TASK [debug] ************
ok: [test_03] => {
"inventory_hostname": "test_03"
}

ansible vars_files and extra_vars to read input

looking to pass the dict to read a set of key value pairs based on the location. When values are hardcoded to the playbook, it works fine but calling through extra_vars giving an error message. Not sure even if it supports. appreciate, your thoughts and inputs.
ansible-playbook play3.yml -e '{"var1":"loc2"}' -vv
play3.yml
---
- name: testing
hosts: localhost
connection: local
gather_facts: no
vars_files:
- var_file.yml
tasks:
- debug:
msg: "{{ var1['first'] }}"
var_file.yml
---
loc1:
first: name1
last: name2
loc2:
first: python
last: perl
...
"Anything's possible in an animated cartoon." -Bugs Bunny
This playook:
---
- name: testing
hosts: localhost
connection: local
gather_facts: no
vars_files:
- var_file.yml
tasks:
- debug:
var: "{{ item }}.first"
with_items: "{{ var1 }}"
Gave me this output:
TASK [debug] **********************************************************************************************************************************
task path: /home/jack/Ansible/CANES/PLAYBOOKS/play3.yml:9
ok: [localhost] => (item=None) => {
"loc2.first": "python"
}

Resources