$ ansible --version
ansible 2.10.8
I believe this is a different issue from Ansible's set_fact not working at all.
I have the following variable defined in group_vars/all.yml
nexus_repo = "dev_builds"
nexus_url = "https://nexus.company.com/{{ nexus_repo }}"
Then on one of my tasks, I do
- name: Adjust nexus_repo value for release builds
set_fact:
nexus_repo: "release_builds"
nexus_url: "https://nexus.company.com/{{ nexus_repo }}"
- debug: msg="{{ nexus_url }}"
When I run my playbook (my-playbook.yml runs my-role.yml), I just see
$ ansible-playbook -i inverntories/hosts.yml -e var_hosts=my-host my-playbook.yml
TASK [my-role : Adjust nexus_repo value for release builds] ****************
ok: [10.227.x.x]
TASK [my-role : debug] *****************************************************
ok: [10.227.x.x] => {
"msg": "https://nexus.mycompany.com/repository/dev_builds"
}
Why is that?
The problem is that you can't re-use variables declared in the same set_fact. For example, if the variables nexus_repo and nexus_url are declared for the first time in the set_fact below. The task
- set_fact:
nexus_repo: release_builds
nexus_url: "https://nexus.company.com/{{ nexus_repo }}"
will fail because nexus_url can't use next_repo declared at the previous line:
The task includes an option with an undefined variable. The error was: 'nexus_repo' is undefined
This explains the 'strange' behavior you see when the group_vars/all.yml is used
shell> cat group_vars/all.yml
nexus_repo: dev_builds
nexus_url: "https://nexus.company.com/{{ nexus_repo }}"
The set_fact below will use the value dev_builds of the variable nexus_repo from group_vars/all.yml when evaluating nexus_url
- set_fact:
nexus_repo: release_builds
nexus_url: "https://nexus.company.com/{{ nexus_repo }}"
- debug:
var: nexus_url
gives
nexus_url: https://nexus.company.com/dev_builds
There are more options on how to fix it. For example, don't declare the same variable nexus_url twice
- set_fact:
nexus_repo: release_builds
- debug:
var: nexus_url
gives
nexus_url: https://nexus.company.com/release_builds
If you have to declare nexus_url new put it into the separate set_fact. For example, the tasks below give the same result
- set_fact:
nexus_repo: release_builds
- set_fact:
nexus_url: "https://nexus.company.com/{{ nexus_repo }}"
- debug:
var: nexus_url
See the Ansible issue Can't reference a dict key inside the same dict #50280.
Related
The Ansible Documentation on conditionals provides examples of how a task can check whether a variable is defined:
- name: Fail if "bar" is undefined
ansible.builtin.fail: msg="Bailing out. This play requires bar"
when: bar is undefined
I'm building a playbook that checks whether a list of variables is defined. My first insinct was to write the code like this:
- name: Fail if any variables are undefined
ansible.builtin.fail: msg="Bailing out. This play requires {{ item }}"
when: item is undefined
with_items:
- bar
- foo
- baz
However, this doesn't work, since this is checking that the item variable itself is defined.
Use lookup plugin varnames. See
shell> ansible-doc -t lookup varnames
For example, if none of the listed variables is defined
- set_fact:
my_vars: "{{ my_vars|d({})|combine({item: _defined}) }}"
loop:
- bar
- foo
- baz
vars:
_defined: "{{ lookup('varnames', item)|length > 0 }}"
gives
my_vars:
bar: false
baz: false
foo: false
Test it. For example, the task
- assert:
that: my_vars_undef|length == 0
fail_msg: "Bailing out. This play requires: {{ my_vars_undef }}"
vars:
my_vars_undef: "{{ my_vars|dict2items|
rejectattr('value')|
map(attribute='key')|
join(',') }}"
will fail
TASK [assert] ***********************************************************
fatal: [localhost]: FAILED! => changed=false
assertion: my_vars_undef|length == 0
evaluated_to: false
msg: 'Bailing out. This play requires: bar,foo,baz'
Example of a playbook
shell> cat pb.yml
- hosts: localhost
tasks:
- set_fact:
my_vars: "{{ my_vars|d({})|combine({item: _defined}) }}"
loop:
- bar
- foo
- baz
vars:
_defined: "{{ lookup('varnames', item)|length > 0 }}"
- assert:
that: my_vars_undef|length == 0
fail_msg: "Bailing out. This play requires: {{ my_vars_undef }}"
vars:
my_vars_undef: "{{ my_vars|dict2items|
rejectattr('value')|
map(attribute='key')|
join(',') }}"
The playbook will continue if all variables in the list are defined
shell> ansible-playbook pb.yml -e bar=A -e foo=B -e baz=C
...
TASK [assert] **********************************************************
ok: [localhost] => changed=false
msg: All assertions passed
It's technically possible to do this with a template string:
- name: Fail if any variables are undefined
ansible.builtin.fail: msg="Bailing out. This play requires {{ item }}"
when: "{{ item }} is undefined"
with_items:
- bar
- foo
- baz
but it triggers the following warning from Ansible:
[WARNING]: conditional statements should not include jinja2 templating delimiters such as {{ }} or {% %}. Found: {{ item }} is undefined
This is my ansible script.
- name: Validate that blacklisted URLs are unreachable
environment:
SSL_CERT_FILE: "{{ ssl_cert_path }}"
ansible.builtin.uri:
url: "{{ item }}"
timeout: 10
register: blacklisted_http_responses
with_lines: cat {{ role_path }}/files/blacklisted_urls.txt
And i am getting this lint error for the sbove code
Found a bare variable 'cat {{ role_path }}/files/blacklisted_urls.txt' used in a 'with_lines' loop.
any idea how to resolve this ? I tried Putting the variable name in double quotes.
What you see is very probably an ansible-lint issue. You should use loop instead of with_lines. There are no complaints by ansible-lint about the code below
loop: "{{ lookup('file',
role_path ~ '/files/blacklisted_urls.txt').splitlines() }}"
You can also use the pipe lookup plugin instead of the file if you want to. The loop below gives the same result
loop: "{{ lookup('pipe',
'cat ' ~ role_path ~ '/files/blacklisted_urls.txt').splitlines() }}"
For example, the playbook
shell> cat pb.yml
---
- hosts: localhost
roles:
- role_a
the role
shell> cat roles/role_a/tasks/main.yml
---
- name: Debug
debug:
var: item
loop: "{{ lookup('file',
role_path ~ '/files/blacklisted_urls.txt').splitlines() }}"
and the file
shell> cat roles/role_a/files/blacklisted_urls.txt
www1.example.com
www2.example.com
give (abridged)
TASK [role_a : Debug] ****************************************************
ok: [localhost] => (item=www1.example.com) =>
ansible_loop_var: item
item: www1.example.com
ok: [localhost] => (item=www2.example.com) =>
ansible_loop_var: item
item: www2.example.com
In foo.yml, the dev.foo variable contains a value of bar.
---
- hosts: all
vars:
dev:
foo: bar
I set the env variable to contain a value of dev on the command line.
ansible-playbook foo.yml --extra-vars "env=dev"
If I attempt to debug env.foo . . .
tasks:
- debug:
msg: "{{ env.foo }}"
The following is returned.
TASK [debug]
fatal: [server1.example.com]: FAILED! => {
"msg": "The task includes an option with an undefined variable.
The error was: 'str object' has no attribute 'foo'"
}
I am not sure how to resolve env to dev in jinja2 and then access nested variable dev.foo.
Indirect addressing is not available in Ansible. You can use vars lookup instead. See ansible-doc -t lookup vars e.g.
- debug:
msg: "{{ lookup('vars', env).foo }}"
gives
msg: bar
TL/DR: I have "src_host" and "dest_host" variables that I want to use to set the "- hosts:" object in a play. However, I have to set them again for each play under "vars:" of each "- hosts:" section e.g. src_host="{{ hostvars['localhost']['src_host'] }}" how do I set these two variables at the beginning and not have to reset them?
My hosts file looks like this
[wordpress]
localhost ansible_user=user ansible_port=22 ansible_ssh_private_key_file=/home/user/.ssh/id_rsa
root_localhost ansible_user=root ansible_port=22 ansible_ssh_private_key_file=/home/user/.ssh/id_rsa
---snip---
server2.net ansible_host="server2.net" ansible_user=user ansible_port=22 ansible_ssh_private_key_file=/home/user/.ssh/id_rsa
root_server2.net ansible_host="server2.net" ansible_user=root ansible_port=22 ansible_ssh_private_key_file=/home/user/.ssh/id_rsa
The beginning of my playbook looks like this:
- hosts: localhost, server2.net, root_server2.net #always include "localhost" in this list because it is needed to store the variables for the src_host and dest_host
vars:
src_host: localhost #modify these and the host will be changed for all subsequent plays/tasks
dest_host: server2.net #modify these and the host will be changed for all subsequent plays/tasks
src_dump_path: /home/user/cvrt9_dump.sql #set vars for copying file
roles:
- set_facts_for_db_copy
- hosts: "{{ src_host }}"
vars:
src_host: "{{ hostvars['localhost']['src_host'] }}"
dest_host: "{{ hostvars['localhost']['dest_host'] }}"
---snip---
roles:
- dump_db
- copy_file
etc . . .
for "- set_facts_for_db_copy" I have "main.yml" as this where I set the "src_host" and "dest_host" variables:
---
# tasks file for set_facts_for_db_copy
- name: create variables that equal src_dump_path and set src_host/dest_host
set_fact:
---snip---
src_host: "{{ src_host }}"
dest_host: "{{ dest_host }}"
So I need to set the "src_host" and "dest_host" for all subsequent "- hosts:" that use them by getting the values from one of the host variables that "set_fact_for_db_copy" set. I randomly picked "localhost" as you may have noticed:
src_host: "{{ hostvars['localhost']['src_host'] }}"
dest_host: "{{ hostvars['localhost']['dest_host'] }}"
If I don't have that line there I get:
user#localhost:/home/maintainer/ansible-play$ ansible-playbook -i hosts_tat-kay playbook.yml
PLAY [localhost, server2.net, root_server2.net] **************
TASK [setup] *******************************************************************
ok: [server2.net]
ok: [root_server2.net]
ok: [localhost]
TASK [set_facts_for_db_copy : create variables that equal src_dump_path] *******
ok: [localhost]
ok: [server2.net]
ok: [root_server2.net]
ERROR! the field 'hosts' has an invalid value, which appears to include a variable that is undefined. The error was: 'src_host' is undefined
The error appears to have been in '/home/maintainer/ansible-play/playbook.yml': line 14, column 3, but may
be elsewhere in the file depending on the exact syntax problem.
The offending line appears to be:
- hosts: "{{ src_host }}"
^ here
. . .
Now I can set the these variables in my host file:
[wordpress:vars]
src_host=localhost
dest_host=server2.net
But then I still have to reference them from the subsquent "-hosts:" objects in my playbook with "{{ hostvars['localhost']['src_host'] }}" etc . . . So my question is how do I get rid of this redundant code in all my subsequent "-hosts:" objects (shown below) while still letting me change the "src_host" and "dest_host" variables once at the beginning and have those changes affect the rest of the plays? Thanks.
src_host: "{{ hostvars['localhost']['src_host'] }}"
dest_host: "{{ hostvars['localhost']['dest_host'] }}"
For this use your inventory file, make a parent group of the host you need the variable as follow.
[desireenv:children]
wordpress
otherhost
etc
and then assigne th vars value to the new parent group created
[desireenv:vars]
src_host: "{{ hostvars['localhost']['src_host'] }}"
dest_host: "{{ hostvars['localhost']['dest_host'] }}"
One solution I found with the help of https://stackoverflow.com/users/4716639/bryan-calvo-benoit is to put this in my hosts file (inventory file)
[wordpress]
localhost
server2.net
[testenv:children]
wordpress
[testenv:vars]
src_host=localhost
dest_host=server2.net
And then in the ansible playbook and the roles that it calls I had to replace
"{{ src_host }}"
with
"{{ hostvars['localhost']['src_host'] }}"
and likewise for "{{ dest_host }}"
However, I could delete this redundant code in my ansible playbook:
src_host: "{{ hostvars['localhost']['src_host'] }}"
dest_host: "{{ hostvars['localhost']['dest_host'] }}"
It would be nice if I didn't have to change the src_host and dest_host to hostvars['localhost']... because it seems arbitrary to use localhost and also what if I want to run several ansible scripts one right after the other with different src_host and dest_host? Using the inventory file locks it down so this is not ideal. If no one else answers I'll accept this answer because it is the only one that works and it technically does what my question asked.
How do I get a value from an environment variable, but use a default if the environment variable is unset?
This is an example that does not work
---
- name: a playbook
hosts: all
vars:
build_dir: "{{ lookup('env','BUILD_DIR') | default('builds/1.0.0/LATEST') }}"
tasks:
- debug: msg="{{ build_dir }}"
Running this playbook returns an empty string instead of the default.
$ ansible-playbook build.yml
TASK [debug] ********************
ok: [amber] => {
"msg": ""
}
However, it works as expected to obtain the environment variable.
$ BUILD_DIR=LOL ansible-playbook build.yml
TASK [debug] ****************
ok: [amber] => {
"msg": "LOL"
}
Discovered this that is more concise and easier to read than some other options I have seen
"{{ lookup('env','BUILD_DIR') or 'builds/1.0.0/LATEST' }}"
The last parameter to Jinja's default template built-in function should be true, like this:
vars:
build_dir: "{{ lookup('env','BUILD_DIR')|d('builds/1.0.0/LATEST', true) }}"
Better not to have too many sources of truth, but I always try to set intelligent defaults in defaults/main.yml. I also make frequent use of the default() filter, like this:
db_url : "{{ DB_HOST }}:{{ db_port | default(1521) }}:{{ DB_SVC | default(SID|default('')) }}"
Then a playbook can always overwrite a role's variable with a lookup that defaults to a literal -
vars:
db_port: "{{ lookup('env','db_port')|default('9999') }}"
or with a value dynamically written into a vars_file before the play begins, or into the hosts file or groups file, or on the ansible command-line with --extra-vars, etc.
Look at the variable precedence rules, but be careful not to get too complex if it can be avoided. Flexibility is good, but KISS, else "that way lies madness..."