Ansible inconsistent value for ansible_fqdn - ansible

The variable {{ansible_fqdn}} can take two different values: the server's short name (server300) or its long name (server300.prod.x.y.z).
When we use this variable in the playbook to retrieve a file {{ansible_fqdn}}.crt, depending on the value chosen, the file can be not found, and the playbook will fail.
To have a consistent value for hostnames over a list of servers, should one rather use {{ansible_hostname}} (short hostname from linux command uname -n) and {{inventory_hostname}} (long hostname from "hosts" file) instead?
Or is there a way to obtain a consistent value from {{ansible_fqdn}}?

Q: "The variable {{ ansible_fqdn }} can take two different values: the server's short name (server300) or its long name (server300.prod.x.y.z)."
A: It depends on how you configure hostname. Take a look at uname -n. Either it is long
[admin#test_23 ~]$ uname -n
test_23.example.com
shell> ansible test_23 -m setup | grep ansible_fqdn
"ansible_fqdn": "test_23.example.com",
, or it is short
[admin#test_11 ~]$ uname -n
test_11
shell> ansible test_11 -m setup | grep ansible_fqdn
"ansible_fqdn": "test_11",
Q: "Should one rather use {{ ansible_hostname }} (short hostname from linux command uname -n) and {{ inventory_hostname }} (long hostname from "hosts" file) instead?"
A: It depends on how you configure the inventory. The variable ansible_hostname is not required. If you don't run setup (or get the variables from a cache) and if you don't declare it explicitly it won't be defined, e.g.
shell> cat hosts
test_23
shell> ansible test_23 -m debug -a var=inventory_hostname
test_23 | SUCCESS => {
"inventory_hostname": "test_23"
}
shell> ansible test_23 -m debug -a var=ansible_hostname
test_23 | SUCCESS => {
"ansible_hostname": "VARIABLE IS NOT DEFINED!"
}
You can declare alias and ansible_hostname, e.g.
shell> cat hosts
alias_of_test_23 ansible_hostname=test_23
shell> ansible alias_of_test_23 -m debug -a var=inventory_hostname
alias_of_test_23 | SUCCESS => {
"inventory_hostname": "alias_of_test_23"
}
shell> ansible alias_of_test_23 -m debug -a var=ansible_hostname
alias_of_test_23 | SUCCESS => {
"ansible_hostname": "test_23"
}
If you run setup the value of ansible_hostname is the short hostname from the command uname -n, e.g.
shell> cat hosts
alias_of_test_23 ansible_host=test_23.example.com
shell> ansible alias_of_test_23 -m setup | grep ansible_hostname
"ansible_hostname": "test_23",
Q: "Or is there a way to obtain a consistent value from {{ ansible_fqdn }}?"
A: There are more options:
At remote hosts, configure hostnames to provide you with the FQDN
In the inventory, declare the FQDN form of aliases and use inventory_hostname
If the options above are not feasible you'll have to declare a custom variable.
The consistency is up to you.

Related

Ansible variable manipulation within vars

I want to construct the password (ansible_ssh_pass) within the vars section of my playbook from a string passed as an input (pass_var). The problem here is that the actual password should be the first 6 characters from the variable (pass_var). I am not sure how to achieve it. Here is my playbook
- hosts: all
user: "{{username}}"
become: yes
gather_facts: False
vars:
ansible_ssh_pass: <someway to decode string (base64 -d) and get first 6 characters>
I'm running the playbook as:
ansible-playbook test.yml --extra-vars "pass_var=${pass_val} username=${user}"
I also need to do some shell manipulations. For example, my string will be base64 encoded, so I need to decode it as well. Something like: echo ${pass_var} | base64 -d | rev | cut -c1-6
You can use Python-style string slicing in Ansible, so you can just write:
vars:
ansible_ssh_pass: "{{ pass_var[:6] }}"
For example, the following command:
ansible localhost -e pass_var=1234567890 -m debug -a 'msg={{pass_var[:6]}}'
Will output:
localhost | SUCCESS => {
"msg": "123456"
}
If your initial string is base64 encoded, you can use Ansible's b64_decode filter:
ansible_pass: "{{ (pass_var|b64decode)[:6] }}"
And if for some weird reason you need to reverse it, there is a reverse filter:
ansible_pass: "{{ ((pass_var|b64decode)|reverse)[:6] }}"
If we modify my earlier example, we get:
ansible localhost -e pass_var="MTIzNDU2Nzg5MA==" -m debug -a 'msg={{((pass_var|b64decode)|reverse)[:6]}}'
Which produces:
localhost | SUCCESS => {
"msg": "098765"
}

Ansible inventory file with same vcenter hostname only takes the last entry

I am trying to run a playbook to create disks on VMs which are on different Vcenters.
so when I put them in an inventory file something like this:
**inv.yml:
vcenter1 datacenter=dc1 datastore=ds1 name=vm1
vcenter1 datacenter=dc1 datastore=ds1 name=vm2
vcenter1 datacenter=dc1 datastore=ds1 name=vm3
vcenter2 datacenter=dc2 datastore=ds2 name=vm4
vcenter2 datacenter=dc2 datastore=ds2 name=vm5
vcenter3 datacenter=dc3 datastore=ds3 name=vm6
vcenter3 datacenter=dc3 datastore=ds3 name=vm6**
When I run this it reads only once for each vcenter and ignores other entries.
Any way I can make Ansible read same hostname again and again.
I am able to do this with a variable file but I am trying to do this with an inventory file.
Q: "Any way I can make Ansible read same hostname again and again?"
A: No. There isn't any. Change the data instead. For example
shell> cat inv.yml
vcenter1 datacenter=dc1 datastore=ds1 name=[vm1,vm2,vm3]
vcenter2 datacenter=dc2 datastore=ds2 name=[vm4,vm5]
vcenter3 datacenter=dc3 datastore=ds3 name=[vm6]
shell> cat test.yml
- hosts: all
tasks:
- debug:
var: name
shell> ansible-playbook -i inv.yml test.yml
ok: [vcenter1] =>
name: '[vm1,vm2,vm3]'
ok: [vcenter2] =>
name: '[vm4,vm5]'
ok: [vcenter3] =>
name: '[vm6]'

Unable to resolve ansible_user variable

Setup
Passwords, not keys are used for ssh
The target is accessed via the bastion host (ssh -> bastion -> target)
The password is kept in an encrypted file that is accessed based on the ansible_user name
When decrypted, the password is bastion
Command
$ ansible -i ./inventory/debug -m debug -a 'var=foo' -kKu ansible all
Command Output
target | FAILED! => {
"msg": "The field 'ssh_common_args' has an invalid value, which includes an undefined variable. The error was: 'ansible_user' is undefined"
}
bastion | SUCCESS => {
"foo": "-o ProxyCommand=\"sshpass -p bastion ssh -o StrictHostKeyChecking=no -W %h:%p -q ansible#3.21.247.xxx.\""
}
Problem
The password is correctly accessed and resolved for foo for bastion, but foo cannot be resolved for the target host.
The inventory file (called "debug")
all:
vars:
env: 3.21.247.xxx
password: "{{lookup('file', inventory_dir + '/../users/' + ansible_user + '.yml')}}"
foo: "-o ProxyCommand=\"sshpass -p {{password}} ssh -o StrictHostKeyChecking=no -W %h:%p -q ansible#{{env}}.\""
children:
bastions:
hosts:
bastion:
ansible_host: "{{ env }}"
nv:
children:
targets:
hosts:
target:
ansible_host: 10.0.3.209
vars:
ansible_ssh_common_args: "{{ foo }}"
You cannot use jinja2 expansion inside jinja2 expansion. Try:
"{{lookup('file', inventory_dir + '/../users/' + ansible_user + '.yml')}}"

jinja2 expression to reference inventory collection to pass as parameter to shell script

I have a hosts file myEnv with:
[myEnv:children]
app0
app1
app2
And a group file myEnv with:
env: "myEnv"
In the following playbook task I'm attempting to pass environment (value of env key) and list of apps in that environment (myEnv:children) to a shell script as parameters. Only the environment ('myEnv') is getting passed to the script. I'm unable to figure out correct jinja2 syntax to pass the list of apps associated with this key which is in my hosts file.
- name: Run createFacts.sh in bin directory
command: ./createFacts.sh {{ env }} {{ hostvars[env] }}
register: createPuppetFacts
args:
chdir: "{{binHome}}"
What jinja2 syntax do I require for this? I've scoured ansible docs and stack overflow and just not finding right format...other than errors in syntax the best I can do is get an empty string back!
Much appreciate help on this.
Q: "Pass environment (value of env key) and list of apps in that environment (myEnv:children) to a shell script as parameters."
A: Given the inventory
$ cat hosts
test_01
test_02
test_03
[app0]
test_01
test_02
test_03
[app1]
test_01
test_02
[app2]
test_02
test_03
[myEnv:children]
app0
app1
app2
and the group_vars
$ cat group_vars/myEnv
env: "myEnv"
The playbook
$ cat playbook.yml
- hosts: test_01
tasks:
- command: "/home/admin/createFacts.sh {{ env }}"
register: result
- debug:
var: result.stdout
- command: "/home/admin/createFacts.sh {{ groups[env] }}"
register: result
- debug:
var: result.stdout
with the script
$ cat /home/admin/createFacts.sh
#!/bin/sh
echo $#
exit 0
gives
ok: [test_01] => {
"result.stdout": "myEnv"
}
ok: [test_01] => {
"result.stdout": "[utest_01, utest_02, utest_03]"
}
Notes
myEnv, app0, app1, app2 are groups. See Inheriting variable values: group variables for groups of groups
The value of env is myEnv. This is the name of a group. It's posible to list the members of the group groups[env]. But hostvars[env] must fail, because hostvars expects the name of a host as a parameter.
Ansible does not provide the runtime with the information that the group myEnv is parent of the groups app0, app1, app2.
There is a special variable group_names: "List of groups the current host is part of"
The task below
- debug:
var: group_names
gives
ok: [test_01] => {
"group_names": [
"app0",
"app1",
"myEnv"
]
}

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

Resources