Ansible import_playbook fails with variable undefined error - ansible

I am trying to conditionally import a playbook by dynamically generating the name of the playbook to import.
I have the following 2 files:
root#ubuntu:~/test# ls -ltr
total 8
-rw-r--r-- 1 root root 117 sij 2 12:07 child_playbook_1.yaml
-rw-r--r-- 1 root root 210 sij 2 12:11 start.yaml
root#ubuntu:~/test#
start.yaml:
---
- name: main playbook
hosts: localhost
tasks:
- set_fact:
var: "child_playbook"
- debug:
msg: "{{ var }}"
- import_playbook: "{{ var + '_1.yaml' }}"
when: var is defined
child_playbook_1.yaml:
---
- name: child_playbook
hosts: localhost
tasks:
- debug:
msg: "Message from the child playbook"
When I execute start.yaml, I get this as output:
root#ubuntu:~/test# ansible-playbook start.yaml
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'
ERROR! 'var' is undefined
root#ubuntu:~/test#
Why is var not seen and why do I get this message?
How to overcome this?
To put things into a bit more perspective, the 'var' variable in start.yaml is being dynamically read which means that the name of the playbook to be imported will also be dynamic.

var is a fact defined for localhost (i.e. the only host in the play) in the first play. So it does not exist "globally", only for the given host.
Moreover, imports are made statically at time of playbook parsing (by opposition to includes which are dynamic but to not exist for playbooks).
One could then try to use the var defined for localhost by referencing hostvars['localhost'].var but:
set_fact has not yet run since it is an import.
Moreover hostvars is not yet defined at time of import.
Lastly, you are miss-interpreting how a when clause is actually working. Basically, the condition is passed to all the tasks contained in your included/imported object. So the imported object must exist. If you use an undefined var to get its name, it will always fire an error.
The only solution I currently see to pass a variable playbook name is to use an extra var on the command line. If you want to be able to 'skip' the import when no variable is defined, you could default to an empty playbook.
Here is a dummy empty.yml playbook
---
- name: Dummy empty playbook
hosts: localhost
gather_facts: false
For my tests, I used the same child playbook as in your question except I disabled facts gathering not needed here:
---
- name: child_playbook
hosts: localhost
gather_facts: false
tasks:
- debug: msg="Message from the child playbook"
The new main playbook start.yml looks like this:
---
- name: Play written in start.yml directly
hosts: localhost
gather_facts: false
tasks:
- debug:
msg: "Message from start playbook"
- import_playbook: "{{ var is defined | ternary( var | default('') + '_1.yml', 'empty.yml') }}"
Note than even though we use var is defined as a condition, var will still be interpreted in the ternary filter in all cases (defined or not). So we have to use a default value to make sure we don't have an error when we don't pass a value.
We can now call the playbook with or without an extra var. Here is the result:
$ ansible-playbook start.yml
PLAY [Play written in start.yml directly] *************************************************************************************************************************************************************************
TASK [debug] ******************************************************************************************************************************************************************************************************
ok: [localhost] => {
"msg": "Message from main playbook"
}
PLAY [Dummy empty playbook] ***************************************************************************************************************************************************************************************
PLAY RECAP ********************************************************************************************************************************************************************************************************
localhost : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
$ ansible-playbook start.yml -e var=child_playbook
PLAY [Play written in start.yml directly] *************************************************************************************************************************************************************************
TASK [debug] ******************************************************************************************************************************************************************************************************
ok: [localhost] => {
"msg": "Message from main playbook"
}
PLAY [child_playbook] *********************************************************************************************************************************************************************************************
TASK [debug] ******************************************************************************************************************************************************************************************************
ok: [localhost] => {
"msg": "Message from the child playbook"
}
PLAY RECAP ********************************************************************************************************************************************************************************************************
localhost : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

Related

How to pass URLs as a variable to an ansible playbook

I created a playbook which clears cache, I'm trying to pass a url to a variable, and when I execute the playbook I get an empty array for that parameter.
In my playbook I have a vars module with this variable (environment), it gets defined when you pass in a variable to the ansible-playbook command
vars:
environment: "{{testenv}}"
-e testenv=https://www.test1.com
When I execute the playbook I get this error.
Do I need to format the url in someway?
fatal: [localhost]: FAILED! => {"changed": false, "msg": "unknown url type: '[]/content/clear_cache?
Your issue is coming from the fact that environment is a reserved variable, as pointed in the second row of this table in the documentation:
Valid variable names
 Not valid
 foo
 *foo, Python keywords such as async and lambda
 foo_env
 playbook keywords such as environment
 foo_port
 foo-port, foo port, foo.port
 foo5, _foo
 5foo, 12
Source: https://docs.ansible.com/ansible/latest/user_guide/playbooks_variables.html#creating-valid-variable-names
So, you just need to change your variable name to something else and it will work.
Given the playbook:
- hosts: all
gather_facts: no
tasks:
- debug:
msg: "{{ _environment }}"
vars:
_environment: "{{ testenv }}"
When run:
$ ansible-playbook play.yml -e testenv=https://www.test1.com
PLAY [all] **********************************************************************************************************
TASK [debug] ********************************************************************************************************
ok: [localhost] => {
"msg": "https://www.test1.com"
}
PLAY RECAP **********************************************************************************************************
localhost : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0

Ansible hosts to be set to a substring of a passed variable

I have a play like this:
- name: Perform an action on a Runtime
hosts: all
roles:
- role: mule_action_on_Runtime
A variable at invocation (--extra-vars 'mule_runtime=MuleS01-3.7.3-Testing') has a prefix of the host needed (MuleS01). I want to set hosts: MuleS01. How do I do this?
Given that your pattern is always PartIWant-PartIDonCareAbout-AnotherPartAfterOtherDash you could use the split method of Python, then get the first item of the list via the Jinja filter first.
Here is full working playbook as example:
- hosts: local
gather_facts: no
tasks:
- debug:
msg: "{{ mule_runtime.split('-') | first }}"
This yield the recap:
play.yml --extra-vars 'mule_runtime=MuleS01-3.7.3-Testing'
PLAY [local] *******************************************************************
TASK [debug] *******************************************************************
ok: [local] => {
"msg": "MuleS01"
}
PLAY RECAP *********************************************************************
local : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
With the inventory
shell> cat hosts
MuleS01
MuleS02
MuleS03
this playbook
shell> cat pb.yml
- hosts: all
tasks:
- debug:
msg: Set {{ mule_runtime }}
when: mule_runtime.split('-').0 == inventory_hostname
gives
skipping: [MuleS02]
ok: [MuleS01] => {
"msg": "Set MuleS01-3.7.3-Testing"
}
skipping: [MuleS03]

Ansible: iterating and pairing inventory items with variables

This is directly related to the following link question, and the answer from larsks, that I tried, but it does not work: "how to loop through inventory and assign value in ansible"
I was trying to do the same, and I tested to rename 2 VMs controlled by Ansible, but I get errors like the next one when I try to run the Playbook (ansible-playbook -i hosts test_iterate_win.yml -vvv), I would say that it is literally taking by name 'System.Object[]' instead of, for example, wCloud2:
failed: [oldVM2] (item=[u'oldVM2', u'wCloud2']) => {
"ansible_loop_var": "item",
"changed": false,
"item": [
"oldVM2",
"wCloud2"
],
"msg": "Failed to rename computer to 'System.Object[]': Skip computer 'oldVM2' with new name 'System.Object[]' because the new name is not valid. The new computer name entered is not properly formatted. Standard names may contain letters (a-z, A-Z), numbers (0-9), and hyphens (-), but no spaces or periods (.). The name may not consist entirely of digits, and may not be longer than 63 characters.",
"old_name": "oldVM2",
"reboot_required": false
}
In my inventory file:
[windows]
oldVM1 ansible_host=192.168.122.6
oldVM2 ansible_host=192.168.122.139
My Playbook:
---
- hosts: windows
gather_facts: false
vars:
hostnames:
- wCloud1
- wCloud2
tasks:
- name: change hostname
win_hostname:
name: "{{ item }}"
loop: "{{ groups.windows|zip(hostnames)|list }}"
What I am doing wrong?
TL;DR;
I would say, you are making this super complex for yourself for nothing, when there could be a simple solution about it.
Your task could be easily resolved by just using host variables in your inventory:
[windows]
oldVM1 ansible_host=192.168.122.6 newName=wCloud1
oldVM2 ansible_host=192.168.122.139 newName=wCloud2
Then your playbook is as easy as:
---
- hosts: windows
gather_facts: false
tasks:
- name: change hostname
win_hostname:
name: "{{ newName }}"
Now, the reason why your attempt didn't work, is actually coming from a bit of a misconception you have on task(s) run on multiple hosts by Ansible, I would say.
Namely, when Ansible have a task (or a set of tasks) to run on multiple hosts, one and only one task need to be defined.
Based on the said task, when the inventory hosts is actually a group of hosts, the task will be run on the host 1, then the host 2, ... until the host n, before it goes to run the next task (if any).
Nota: still, don't take this for granted, there has been know issues where Ansible would not follow the order of definition of the hosts in the inventory (see: https://github.com/ansible/ansible/issues/34861), so really it could end up being host 2, host n, host 1.
Consider this playbook with the above inventory:
---
- hosts: windows
gather_facts: false
tasks:
- name: change hostname
debug:
msg: '{{ newName }}'
- name: another task
debug:
msg: 'some example'
The output of it will be
$ansible-playbook test.yml
PLAY [windows] *****************************************************************
TASK [change hostname] *********************************************************
ok: [oldVM1] => {
"msg": "wCloud1"
}
ok: [oldVM2] => {
"msg": "wCloud2"
}
TASK [another task] ************************************************************
ok: [oldVM1] => {
"msg": "some example"
}
ok: [oldVM2] => {
"msg": "some example"
}
PLAY RECAP *********************************************************************
oldVM1 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
oldVM2 : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Where you can clearly see that the first task (named, change hostname) is processed on all hosts before Ansible could move forward to other tasks in the play.
And really what your System.Object[] error means is that you are trying to feed an object (namely a list) in the win_hostname module's name, that tries to convert it as string somehow and fails badly, because, here is the list that your item variable contains:
[
"oldVM1",
"wCloud1"
]

Ansible: include_role on a loop running unexpected number of times

I am trying to use include_role with items
---
- hosts: cluster
tasks:
- block:
- name: Execute test role
include_role:
name: testrole
with_items:
- 'one'
...
My role is
---
- name: Just debugging
debug:
...
The issue is that it seems that the role is being ran by each host X times per item where X is the number of hosts.
PLAY [cluster] *****************************************************************
TASK [setup] *******************************************************************
ok: [thisNode]
ok: [dww]
TASK [Execute test role] *******************************************************
TASK [testrole : Just debugging] ***********************************************
ok: [thisNode] => {
"msg": "Hello world!"
}
ok: [dww] => {
"msg": "Hello world!"
}
TASK [testrole : Just debugging] ***********************************************
ok: [thisNode] => {
"msg": "Hello world!"
}
ok: [dww] => {
"msg": "Hello world!"
}
PLAY RECAP *********************************************************************
dww : ok=3 changed=0 unreachable=0 failed=0
thisNode : ok=3 changed=0 unreachable=0 failed=0
Why is this happening and how can I fix it?
Ansible hosts:
[cluster]
thisNode ansible_host=localhost ansible_connection=local
dww
I cannot delegate the task because, in the real role, the task must be executed in each of the hosts.
Using allow_duplicates: no still outputs the same.
---
- hosts: cluster
tasks:
- name: Execute test role
include_role:
name: testrole
allow_duplicates: False
with_items:
- 'one'
...
As a workaround you can add allow_duplicates: false to prevent Ansible from running the same role twice with the same parameters.
Clearly the module is looped twice: once with hosts, the other time with the specified items. As it is supposed to runs the action against all hosts, the outer loop gets executed twice.
It's a new module in preview state and this behaviour should probably be files as an issue.
Ansible has an internal parameter BYPASS_HOST_LOOP to avoid such situations and this mechanism should probably be used by this module.
I had the same problem and allow_duplicates: False did not change anything.
It seems that setting serial: 1 on the play and it somehow solved it. This is a workaround that probably will work for a small number of hosts.

Add object to a dictionary in variables

I'd like to add objects to a list in variables like this
system_user:
- user1
system_users: "{{ system_users | union(system_user) }}"
It fails with a recursion error:
AnsibleError: recursive loop detected in template string
Is there any way to solve this? I want to create a definition file for each user in group_vars/all/ and then loop through them in a playbook. I don't want to redefine the list for every new user.
PS: There's a workaround: create variables with user names, like system_user_otto20 but it's not elegant at all.
There is a similar opened issue: https://github.com/ansible/ansible/issues/17906
I suggest you not to use undefined variables in template strings to define them.
As another workaround you could use hash_behaviour=merge with following definitions:
group_vars/all.yml
system_users:
user1:
user2:
book1.yml
- hosts: localhost
gather_facts: false
vars:
system_users:
user3:
user4:
tasks:
- debug: msg="{{ system_users | unique }}"
Running a playbook:
$ ansible-playbook book1.yml
[WARNING]: Host file not found: /etc/ansible/hosts
[WARNING]: provided hosts list is empty, only localhost is available
PLAY [localhost] ***************************************************************
TASK [debug] *******************************************************************
ok: [localhost] => {
"msg": [
"user4",
"user2",
"user3",
"user1"
]
}
PLAY RECAP *********************************************************************
localhost : ok=1 changed=0 unreachable=0 failed=0
unique is used to convert dictionary to unsorted list.

Resources