Playbook host_vars - ansible

Is it possible to set variables for specific hosts in Ansible in the playbook itself, using the the global vars?
So the playbook would be configured something like this:
---
- hosts:
- host-1
- host-2
vars:
host-1: # < - set vars for host-1 specifically
a_var: yes
host-2: # < - set vars for host-2 specifically
a_var: no
I know I using either group_vars, host_vars, an inventory file, or set_fact during runtime is possible, but this is not what I want.
The docs describe "playbook host_vars", but I haven't figured out how that is configured.

What you are referring to is not really in the playbook, per se. But it can be in the directory structure next to the playbook itself.
This is further explained in Organizing host and group variables.
Although you can store variables in the main inventory file, storing separate host and group variables files may help you organize your variable values more easily. Host and group variable files must use YAML syntax. Valid file extensions include ‘.yml’, ‘.yaml’, ‘.json’, or no file extension. See YAML Syntax if you are new to YAML.
Ansible loads host and group variable files by searching paths relative to the inventory file or the playbook file.
Source: https://docs.ansible.com/ansible/latest/user_guide/intro_inventory.html#organizing-host-and-group-variables, emphasis, mine
So, what you can have is this:
.
├── host_vars
│ └── localhost.yml
└── play.yml
Where localhost.yml matches the name of the host we want to target and would contain something like:
foo: bar
And the file play.yml would be the playbook:
- hosts: all
gather_facts: no
tasks:
- debug:
var: foo
Then running it would give the expected:
PLAY [all] **********************************************************************************************************
TASK [debug] ********************************************************************************************************
ok: [localhost] => {
"foo": "bar"
}
PLAY RECAP **********************************************************************************************************
localhost : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

Q: "Set variables for specific hosts in Ansible in the playbook itself, using the global vars."
A: Put the host-specific variables into a global dictionary, e.g. my_hostvars. (De facto, create your own hostvars). For example
- hosts: host-1,host-2
vars:
my_hostvars:
host-1: # < - set vars for host-1 specifically
a_var: yes
host-2: # < - set vars for host-2 specifically
a_var: no
tasks:
- debug:
msg: "{{ my_hostvars[inventory_hostname]['a_var'] }}"
gives
ok: [host-1] =>
msg: true
ok: [host-2] =>
msg: false
It's up to you where you declare the dictionary and how you reference it in the playbook. See Variable precedence: Where should I put a variable?.
Put the variables into your own facts to simplify the access. For example
- set_fact:
my_facts: "{{ my_hostvars[inventory_hostname] }}"
- debug:
var: my_facts.a_var
give
ok: [host-1] =>
my_facts.a_var: true
ok: [host-2] =>
my_facts.a_var: false
You can simplify the access further by setting the variables if needed. (For example, to avoid rewriting a code already using the variables).
- set_fact:
a_var: "{{ my_facts.a_var }}"
b_var: "{{ my_facts.b_var }}"
c_var: "{{ my_facts.c_var }}"
You can use it to set or customize (precedence 19.) the default values if needed. For example,
- set_fact:
a_var: "{{ my_facts.a_var|default('a') }}"
b_var: "{{ my_facts.b_var }}"
c_var: "{{ my_facts.c_var }}"

Related

Is there a way to re-set the "hosts" variable within a playbook?

I am tasked with creating a playbook, where I perform the following operations:
Fetch information from a YAML file (the file contains details on VLANs)
Cycle through the YAML objects and verify which subnet contains the IP, then return the object
The object contains also a definition of the inventory_hostname where to run the Ansible playbook
At the moment, I have the following (snippet):
Playbook:
- name: "Playbook"
gather_facts: false
hosts: "localhost"
tasks:
- name: "add host"
add_host:
name: "{{ vlan_target }}"
- name: "debug"
debug:
msg: "{{ inventory_hostname }}"
The defaults file defines the target_host as an empty string "" and then it is evaluated within another role task, like so (snippet):
Role:
- set_fact:
vlan_object: "vlan | trim | from_json | first"
- name: "set facts based on IP address"
set_fact:
vlan_name: "{{ vlan_object.name }}"
vlan_target: "{{ vlan_object.target }}"
delegate_to: localhost
What I am trying to achieve is to change the hosts: variable so that I can use the right target, since the IP/VLAN should reside on a specific device.
I have tried to put the aforementioned task above the add_host, or even putting in the same playbook, like so:
- name: "Set_variables"
hosts: "localhost"
tasks:
- name: "Set target host"
import_role:
name: test
tasks_from: target_selector
- name: "Playbook"
gather_facts: false
hosts: "localhost"
Adding a debug clause to the above playbook sets the right target, but it is not re-used below, making me think that the variable is not set globally, but within that run.
I am looking for a way to set the target, based on a variable that I am passing to the playbook.
Does anyone have any experience with this?
Global facts are not a thing, at best you can assign a fact to all hosts in the play, but since you are looking to use the said fact to add an host, this won't be a solution for your use case.
You can access facts of another hosts via the hostvars special variable, though. It is a dictionary where the keys are the names of the hosts.
The usage of the role is not relevant to your issue at hand, so, in the demonstration below, let's put this aside.
Given the playbook:
- hosts: localhost
gather_facts: no
tasks:
- set_fact:
## fake object, since we don't have the structure of your JSON
vlan_object:
name: foo
target: bar
- set_fact:
vlan_name: "{{ vlan_object.name }}"
vlan_target: "{{ vlan_object.target }}"
run_once: true
- add_host:
name: "{{ vlan_target }}"
- hosts: "{{ hostvars.localhost.vlan_target }}"
gather_facts: no
tasks:
- debug:
var: ansible_play_hosts
This would yield
PLAY [localhost] *************************************************************
TASK [set_fact] **************************************************************
ok: [localhost]
TASK [set_fact] **************************************************************
ok: [localhost]
TASK [add_host] **************************************************************
changed: [localhost]
PLAY [bar] *******************************************************************
TASK [debug] *****************************************************************
ok: [bar] =>
ansible_play_hosts:
- bar

Specifying multiple groups as hosts from different inventories in an Ansible playbook

I'm looking for a way to specify multiple groups as hosts in an Ansible playbook when the groups are located in separate inventories (this is similar to this question, but different because that question assumes one single inventory).
Say I have a playbook called change_things.yml. Sometimes I want to change things in development, sometimes qa, sometimes production, etc.:
ansible-playbook -i development -i production change_things.yml.
Say there are separate inventories which look roughly like this:
# development inventory
[development]
10.0.0.1
10.0.0.2
The playbook above fails to run when hosts is not explicitly specified.
I have a few problems:
Using hosts: all seems harmful. If a user forgets to explicitly declare an inventory, I would imagine that Ansible inherits from whatever is in /etc/ansible/hosts.
Hard-coding host groups (hosts: development:production) is undesirable because I may want to run something like ansible-playbook -i development -i qa change_things.yml in the future.
I'm looking for a way to maintain separate inventories of hosts, but create playbooks in such a way that they can be executed against multiple combinations of host groups. I do not know how to tell Ansible "use these groups from these inventories".
Q: "Tell Ansible 'use these groups from these inventories'"
A: Dynamically create a new group of hosts in the first play and use it afterward. For example
shell> cat pb.yml
- hosts: localhost
tasks:
- add_host:
name: "{{ item }}"
groups: my_group
loop: "{{ my_groups|from_yaml|map('extract', groups)|flatten }}"
- hosts: my_group
tasks:
- debug:
var: inventory_hostname
Given the inventories
shell> cat hosts_prod
[prod]
prod1
prod2
prod3
shell> cat hosts_qa
[qaA]
qa1
qa2
[qaB]
qa3
shell> cat hosts_devel
[develA]
devel1
devel2
[develB]
devel3
The command
ansible-playbook pb.yml -i hosts_prod -i hosts_qa -i hosts_devel -e "my_groups=[develA,qaB]"
gives (abridged)
PLAY [my_group] **********************************************
TASK [debug] **************************************************
ok: [devel2] =>
inventory_hostname: devel2
ok: [devel1] =>
inventory_hostname: devel1
ok: [qa3] =>
inventory_hostname: qa3
Define any combination of inventories and my_groups on the command-line.
Well if you are indeed separating your different servers in different inventories, then you can name the groups with something common like servers
And then your inventory will follow
## this is development inventory
[servers]
node1.dev.example.org
node2.dev.example.org
## this is qa inventory
[servers]
node1.qa.example.org
node2.qa.example.org
And so your playbook will begin with
- hosts: servers
And you can run this with
ansible-playbook -i development -i qa playbook.yml
Another way, but I have to say that I find this a overly complicated scenario, is to use the ansible_inventory_sources special variable, then align the name of your inventory files with the host groups:
## this is development inventory
[development]
node1.dev.example.org
node2.dev.example.org
## this is qa inventory
[qa]
node1.qa.example.org
node2.qa.example.org
So the playbook will have an horrible:
- hosts: "{{ ansible_inventory_sources | map('basename') | map('regex_replace', '^([^\\.]*).*', '\\1') | list | join(':') }}"
Where
basename would get the file name, e.g. inventory.yml
The regex_replace, is not really needed in your use case, but is borrowed from this answer by #Zeitounator, and allows to remove any file extension, if you do have inventory file extension, like development.yml. And since my inventories do have the .yml extension, this is worth noting.
the list is then join'ed with a colon (:) to fall back on your other question
And so running it, again with
ansible-playbook -i development -i qa playbook.yml
Will generate the host group pattern development:qa.
Here is a demo of what this give (using debug for the sake of the demo, but, as variables can be used in host too, this is the same):
- hosts: localhost
gather_facts: no
tasks:
- debug:
msg: "{{ ansible_inventory_sources | map('basename') | map('regex_replace', '^([^\\.]*).*', '\\1') | list | join(':') }}"
Give the recap:
PLAY [localhost] **************************************************************************************************
TASK [debug] ******************************************************************************************************
ok: [localhost] => {
"msg": "developement:qa"
}
PLAY RECAP ********************************************************************************************************
localhost : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

How can I set an environment variable inside of a playbook, then use it inside of a lookup?

I'm working with community.kubernetes.k8s. When I use the module, I can set K8S_AUTH_KUBECONFIG as an environment variable inside of my playbook and it is respected by the module. When I use the k8s lookup, however, the environment variable isn't available inside of the lookup. Based on the documentation, this behavior is expected.
With this:
- name: "My Playbook"
environment:
K8S_AUTH_KUBECONFIG: "{{ lookup('env', 'HOME') }}/cluster_config/kube_config.yaml"
hosts: localhost
connection: local
tasks:
This prints an empty string:
- name: Look up environment variable
debug: msg="{{ lookup('env', 'K8S_AUTH_KUBECONFIG') }}"
This fails due to that empty string:
- name: Look for a namespace
set_fact:
namespace: >
{{ lookup('community.kubernetes.k8s',
api_version='v1',
kind='Namespace',
name='myNamespace') }}
This works as expected because the environment variable is available to the module:
- name: Create a namespace
community.kubernetes.k8s:
state: present
resource_definition:
apiVersion: v1
kind: Namespace
metadata:
name: "{{ item }}"
I'm wondering whether there is a way to declare an environment variable inside of my playbook and have it accessible from inside of lookups? I don't want to set this in my bashrc or anything like that because I need different settings for different situations.
Alternatively, how would you suggest getting that value into the lookups?
Note that the k8s lookup does accept kubeconfig as an argument which is analogous to having K8S_AUTH_KUBECONFIG set as an environment variable. Is there a way for me to pass a variable to the kubeconfig argument like this?
- name: Look for a namespace
set_fact:
namespace: >
{{ lookup('community.kubernetes.k8s',
kubeconfig={{ my_kube_config }},
api_version='v1',
kind='Namespace',
name='myNamespace') }}
Quoting official documentation "The environment: keyword does not affect Ansible itself, Ansible configuration settings, the environment for other users, or the execution of other plugins like lookups and filters"
- name: "My Playbook"
environment:
K8S_AUTH_KUBECONFIG: "{{ lookup('env', 'HOME') }}/cluster_config/kube_config.yaml"
hosts: localhost
connection: local
tasks:
- shell: echo $K8S_AUTH_KUBECONFIG
register: kubeconfig
- name: Look up environment variable
debug: msg="{{ kubeconfig.stdout }}"
The above playbook would result in following output:
PLAY [My Playbook] **************************************************************************************************************************************************
TASK [Gathering Facts] **********************************************************************************************************************************************
ok: [localhost]
TASK [shell] ********************************************************************************************************************************************************
changed: [localhost]
TASK [Look up environment variable] *********************************************************************************************************************************
ok: [localhost] => {
"msg": "/home/ps/cluster_config/kube_config.yaml"
}
PLAY RECAP **********************************************************************************************************************************************************
localhost : ok=3 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

Change where Ansible looks for files

My directory structure looks like this
playbooks/
Foo.yml
tasks/
Task1.yml
Task2.yml
AllTasks.yml
The Foo.yml playbook has a - import_tasks: tasks/AllTasks.yml task. AllTasks.yml has
- import_tasks: tasks/Task1.yml
- import_tasks: tasks/Task2.yml
This works perfectly fine when I execute playbook Foo.yml. But when I execute a playbook located elsewhere (so not directly in this playbooks directory), the imports no longer work. The reason for this is that they import relative to the location of the imported playbook.
The same happens with tasks using other modules, such as copy. They look for files relative to the playbook location.
Is there a way to make my tasks work for playbooks located in different directories?
I know there is a playbook_dir variable which sadly I cannot override. I also came across inventory_dir, but for whatever reason that one is not defined.
A way to reference files relative to the file the reference is made in would work. Example:
- import_tasks: "{{ current_dir }}/Task1.yml"
- import_tasks: "{{ current_dir }}/Task2.yml"
Something relative to the inventory file of this project would also work. Example:
- import_tasks: "{{ inventory_dir }}/playbooks/tasks/Task1.yml"
- import_tasks: "{{ inventory_dir }}/playbooks/tasks/Task2.yml"
This latter approach would force me to add these paths all over the project though.
Q: "playbook_dir variable which sadly I cannot override"
A: The variable playbook_dir works as expected (but it's not needed in this case; see the second part under the line)
shell> cd /scratch/tmp
shell> cat Foo.yml
- hosts: localhost
tasks:
- import_tasks: "{{ playbook_dir }}/tasks/AllTasks.yml"
shell> cat tasks/AllTasks.yml
- import_tasks: "{{ playbook_dir }}/tasks/Task1.yml"
- import_tasks: "{{ playbook_dir }}/tasks/Task2.yml"
shell> cat tasks/Task1.yml
- debug:
msg: Task1.yml
shell> cat tasks/Task2.yml
- debug:
msg: Task2.yml
shell> cd /tmp
shell> pwd
/tmp
shell> ansible-playbook /scratch/tmp/Foo.yml
PLAY [localhost] ****
TASK [debug] ****
ok: [localhost] => {
"msg": "Task1.yml"
}
TASK [debug] ****
ok: [localhost] => {
"msg": "Task2.yml"
}
PLAY RECAP ****
localhost: ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Q: "Change where Ansible looks for files"
A: No changes are needed. The defaults work fine. See Search paths in Ansible. The values of the variable ansible_search_path below explain why the paths are working
shell> cat Foo.yml
- hosts: localhost
tasks:
- debug:
var: ansible_search_path
- import_tasks: tasks/AllTasks.yml
shell> cat tasks/AllTasks.yml
- debug:
var: ansible_search_path
- import_tasks: Task1.yml
- import_tasks: Task2.yml
shell> cat tasks/Task1.yml
- debug:
msg: Task1.yml
shell> cat tasks/Task2.yml
- debug:
msg: Task2.yml
give
"ansible_search_path": [
"/scratch/tmp"
]
"ansible_search_path": [
"/scratch/tmp/tasks",
"/scratch/tmp"
]
"msg": "Task1.yml"
"msg": "Task2.yml"

Calling roles in a loop using dictionaries from an array as vars

My question is somehow similar to the one posted here, but that doesn't quite answer it.
In my case I have an array containing multiple vars: entries, which I loop over when calling a certain role. The following examples shows the idea:
some_vars_file.yml:
redis_config:
- vars:
redis_version: 6.0.6
redis_port: 6379
redis_bind: 127.0.0.1
redis_databases: 1
- vars:
redis_version: 6.0.6
redis_port: 6380
redis_bind: 127.0.0.1
redis_databases: 1
playbook.yml:
...
- name: Install and setup redis
include_role:
name: davidwittman.redis
with_dict: "{{ dictionary }}"
loop: "{{ redis_config }}"
loop_control:
loop_var: dictionary
...
As far as I understand, this should just set the dictionary beginning with the vars node on every iteration, but it somehow doesn't. Is there any chance to get something like this to work, or do I really have to redefine all properties at the role call, populating them using with_items?
Given the role
shell> cat roles/davidwittman_redis/tasks/main.yml
- debug:
var: dictionary
Remove with_dict. The playbook
shell> cat playbook.yml
- hosts: localhost
vars_files:
- some_vars_file.yml
tasks:
- name: Install and setup redis
include_role:
name: davidwittman_redis
loop: "{{ redis_config }}"
loop_control:
loop_var: dictionary
gives
shell> ansible-playbook playbook.yml
PLAY [localhost] **********************************************
TASK [Install and setup redis] ********************************
TASK [davidwittman_redis : debug] *****************************
ok: [localhost] =>
dictionary:
vars:
redis_bind: 127.0.0.1
redis_databases: 1
redis_port: 6379
redis_version: 6.0.6
TASK [davidwittman_redis : debug] ******************************
ok: [localhost] =>
dictionary:
vars:
redis_bind: 127.0.0.1
redis_databases: 1
redis_port: 6380
redis_version: 6.0.6
Q: "Might there be an issue related to the variable population on role call?"
A: Yes. It can. See Variable precedence. vars_files is precedence 14. Any higher precedence will override it. Decide how to structure the data and optionally use include_vars (precedence 18). For example
shell> cat playbook.yml
- hosts: localhost
tasks:
- include_vars: some_vars_file.yml
- name: Install and setup redis
include_role:
name: davidwittman_redis
loop: "{{ redis_config }}"
loop_control:
loop_var: dictionary
Ultimately, command line --extra-vars would override all previous settings
shell> ansible-playbook playbook.yml --extra-vars "#some_vars_file.yml"
Q: "Maybe it is not possible to set the vars section directly via an external dictionary?"
A: It is possible, of course. The example in this answer clearly proves it.

Resources