71

Here is my problem I need to use one variable 'target_host' and then append '_host' to it's value to get another variable name whose value I need. If you look at my playbook. Task nbr 1,2,3 fetch the value of variable however nbr 4 is not able to do what I expect. Is there any other way to achieve the same in ansible?

   ---
    - name: "Play to for dynamic groups"
      hosts: local 
      vars:
        - target_host: smtp
        - smtp_host: smtp.max.com
      tasks:
        - name: testing
          debug: msg={{ target_host }}
        - name: testing
          debug: msg={{ smtp_host }}
        - name: testing
          debug: msg={{ target_host }}_host
        - name: testing
          debug: msg={{ {{ target_host }}_host }}


Output:

TASK: [testing] *************************************************************** 
ok: [127.0.0.1] => {
    "msg": "smtp"
}

TASK: [testing] *************************************************************** 
ok: [127.0.0.1] => {
    "msg": "smtp.max.com"
}

TASK: [testing] *************************************************************** 
ok: [127.0.0.1] => {
    "msg": "smtp_host"
}

TASK: [testing] *************************************************************** 
ok: [127.0.0.1] => {
    "msg": "{{{{target_host}}_host}}"
}
Max
  • 1,202
  • 1
  • 13
  • 21

10 Answers10

74

If you have a variable like

vars: myvar: xxx xxx_var: anothervalue

the working Ansible syntax:

- debug: msg={{ vars[myvar + '_var'] }}

will give you the analogue of:

- debug: msg={{ xxx_var }}

  • 4
    This worked for me like a charm, does exactly what it needs to do. Plus it looks cleaner. Should probably be the accepted answer. – Mohamed O. Abdallah Aug 03 '18 at 12:02
  • 1
    This worked for me as well, should be the accepted answer – niekname Sep 06 '18 at 10:40
  • 8
    It works as of Ansible 2.7, but note that this won't expand nested variable values. Also, `vars` is considered to be "for internal use only", so this may break without warning. – EM0 Feb 07 '19 at 15:43
  • 1
    Yes, but it works with other variables. For instance, from VMWare, I was getting `guest_facts.instance.hw_eth0.macaddress`. And for `eth1`. Now, I need to loop over my interfaces and get those MAC addresses in a string. So I used `"{{ guest_facts['instance']['hw_'+item.interface]['macaddress']}}"`. – Jack Feb 15 '19 at 15:37
26

This is possible as of Ansible 2.5 with the vars lookup plugin, which I think is less likely to break without warning than some of the other methods posted here. For example:

---
 - name: "Example of dynamic groups"
   hosts: localhost
   vars:
     - target_host: smtp
     - smtp_host: smtp.max.com
   tasks:
     - name: testing
       debug: msg={{ lookup('vars', target_host + '_host') }}

Output:

PLAY [Example of dynamic groups] **************************************************

TASK [Gathering Facts] **************************************************
ok: [localhost]

TASK [testing] **************************************************
ok: [localhost] => {
    "msg": "smtp.max.com"
}

edaemon
  • 819
  • 8
  • 7
13

You can use "hostvars" to pass the variable, host facts can be loaded from group vars or host vars

yml

---
- name: "Play to for dynamic groups"
  hosts: x0
  vars:
    - target_host: smtp
  tasks:
    - set_fact: smtp_host="smtp.max.com"
    - set_fact: host_var_name={{target_host}}_host
    - set_fact: dym_target_host={{hostvars[inventory_hostname][host_var_name]}}

    - name: testing
      debug: msg={{ target_host }}
    - name: testing
      debug: msg={{ smtp_host }}
    - name: testing
      debug: msg={{ target_host }}_host
    - name: testing
      debug: msg={{ dym_target_host }}

output:

PLAY [Play to for dynamic groups] *********************************************

GATHERING FACTS ***************************************************************
ok: [x0]

TASK: [set_fact smtp_host="smtp.max.com"] *************************************
ok: [x0]

TASK: [set_fact host_var_name=smtp_host] **************************************
ok: [x0]

TASK: [set_fact dym_target_host={{hostvars[inventory_hostname][host_var_name]}}] ***
ok: [x0]

TASK: [testing] ***************************************************************
ok: [x0] => {
    "msg": "smtp"
}

TASK: [testing] ***************************************************************
ok: [x0] => {
    "msg": "smtp.max.com"
}

TASK: [testing] ***************************************************************
ok: [x0] => {
    "msg": "smtp_host"
}

TASK: [testing] ***************************************************************
ok: [x0] => {
    "msg": "smtp.max.com"
}

PLAY RECAP ********************************************************************
x0                         : ok=8    changed=0    unreachable=0    failed=0
lichon
  • 141
  • 1
  • 5
8

You need to put quotes around it:

- hosts: local
  vars: [ target_host: smtp ]
  tasks:
    debug: msg="{{ target_host }}_host"

-- edit --

Kashyap I need to go one more level than this. Imagine there is another variable 'smtp_host' and I want to construct that variable at runtime using another variable(target_host) and attaching a string '_host' to it. = {{ {{ target_host }}_host }} – Max

My bad. Didn't read carefully enough.

This (AFAIK) isn't possible. The primary limitation that stops us doing this (no matter how you spin it), is 'variable expansion' in ansible is a single pass process and what you want requires multiple-passes.

Only [seriously hacky] ways I can think of are:

  • Create the playbook dynamically from your playbook using template and execute it.
  • I heard that Jinja2 engine does multi-pass evaluation. May be if you put these strings in a template and then use the lookup('template', ...) filter. Unfortunately I have no experience with Jinja2 templates so not quite sure if this is even an option.
Kashyap
  • 15,354
  • 13
  • 64
  • 103
  • Kashyap I need to go one more level than this. Imagine there is another variable 'smtp_host' and I want to construct that variable at runtime using another variable(target_host) and attaching a string '_host' to it. = {{ {{ target_host }}_host }} – Max Mar 26 '15 at 14:45
  • 14
    Every time someone calls 'ansible-playbook test.yml -i hosts' inside a playbook a kitten dies. – J0hnG4lt Mar 27 '15 at 16:37
  • templates look like the most reasonable solution here. – Max Dec 21 '15 at 09:15
  • I don't like cats, so here goes with calling Ansible from Ansible. – Graham Nicholls Jun 12 '18 at 13:22
  • 2
    FYI this answer is outdated -- this is possible as of Ansible 2.5 with the [`vars` lookup plugin](https://docs.ansible.com/ansible/latest/plugins/lookup/vars.html). – edaemon May 08 '20 at 19:43
6

I'm currently using the array-like syntax of Jinja 2. I don't think this is a great solution, but I've yet to find something better.

Let me give an example with one of my abstracted tasks. See my variable configuration and example task below:

# Variables file, available in the task context
containers:
  app:
    image: mynamespace/myappcontainer:snapshot
  web:
    image: nginx:latest
  db:
    image: mariadb:latest



# Example task
- name: Start containers
  docker_container:
    name: "{{ item }}"
    image: "{{ containers[item].image }}"
  with_items:
    - app
    - web
    - db

In the above example I'm using the with_items Ansible loop, which runs the task for each item and makes the {{ item }} variable available accordingly.
This results in creating 3 Docker containers each with the proper container name based on the items list, and the proper image retrieved from the external variables I've configured.

Even though this example uses with_items, it is of course adaptable to your problem with use of your own variables.

Although this works perfectly fine in this situation, I'm afraid this requires the variables you'd like to access to be part of some parent variable (containers in this example). Therefore I'd recommend to split variables with a . to construct a hierarchy, and not with a _.

A variable like a.b.c where b is dynamic, would be accessible using a[b].c.
A variable like a.b where b is dynamic, would be accessible using a[b].


A solution you would use might look like (untested):

- name: "Play to for dynamic groups"
  hosts: local 
  vars:
    - target: smtp
    - hosts:
        smtp: smtp.max.com
        imap: imap.max.com
  tasks:
    - name: testing
      debug: msg={{ hosts[target] }}

Note that the variables are configured slightly differently, because it's structure is hierarchical.

Tim Visée
  • 2,988
  • 4
  • 45
  • 55
  • This looks like a neat way to achieve what I wanted. Important point to note from @kashyap's explanation is "'variable expansion' in ansible is a single pass process" – Max Oct 18 '17 at 06:13
  • I don't think it is single pass, as the variable inside the `[ ]` is evaluated and it's value is used as key. Multiple brackets can be chained together if desired. Anyhow, good to hear this might be useful to you! – Tim Visée Oct 18 '17 at 11:24
  • Wil see if I can put a lid to it by looking at the source or may be taking help from the developers community contributing to ansible. Thank you ! – Max Oct 21 '17 at 16:35
5

You have two ways to choose:
1. General using.

vars:
    - target_host: smtp
    - smtp: smtp.max.com
tasks: 
    - name: testing
        debug: msg={{ target_host }}
    - name: testing
        debug: msg={{ smtp }}
    - name: testing
        debug: msg={{ vars[target_host] }}

2. Using fact

tasks: 
    - set_fact: target_host=smtp
    - set_fact: smtp=smtp.max.com
    - name: testing
        debug: msg={{ target_host }}
    - name: testing
        debug: msg={{ smtp }}
    - name: testing
        debug: msg={{hostvars[inventory_hostname][target_host]}}
Xinyuan.Yan
  • 116
  • 1
  • 7
2

You can try global array var:

regions:
  us-east-1:
    endpoint: rds.us-east-1.amazonaws.com
  cn-north-1:
    endpoint: rds.cn-north-1.amazonaws.com.cn
  cn-northwest-1:
    endpoint: rds.cn-northwest-1.amazonaws.com.cn

And use it to get value depending of another:

region_endpoint: "{{ regions[region].endpoint}}"

In your case:

target_host:
  imap:
    host: imap.max.com
  smtp:
    host: smtp.max.com

then:

region_endpoint: "{{ target_host[service].host}}"
Romain DEQUIDT
  • 792
  • 8
  • 15
1

You can nest your lookups like so:

---
- hosts: local
  connection: local
  gather_facts: no
  vars:
    target_host: smtp
    lookup_host: "{{ target_host }}_host"
    smtp_host: smtp.max.com
  tasks:
    - debug: var="{{ lookup_host }}"
J0hnG4lt
  • 4,337
  • 5
  • 24
  • 40
  • 2
    This would only work with teh special case of `debug: var=` because it's special. You can't pass it to another module. E.g. `debug: msg="{{ lookup_host }}"` or `shell: echo "{{ lookup_host }}"` – Kashyap Mar 27 '15 at 16:55
0

Seems to me you can just use the var option instead of msg:

  debug: var="{{ target_host }}_host"

Gives:

TASK [testing] ********************************************************************************************************************************
ok: [localhost] => {
    "smtp_host": "smtp.max.com"
}
Jack
  • 5,801
  • 1
  • 15
  • 20
0

There are many answers above. It helped me a lot. But i have not found a single answer how to keep these variable in vars-> main.yml file. So you have to create a dictionary in vars-> main.yml file.

vars -> main.yml file

cassandra_version: 2.1.16
file_sha_cassandra: "apache-cassandra-{{ cassandra_version }}_sha256"
# Create Dictionary here
cassandra:
    apache-cassandra-2.1.16_sha256: "sha256: checksum_of_file"
    ##Add more variable

Now you have to call this in task -> main.yml file:

---
  - name: Down load Cassandra Binaries
    get_url:
       url: "file_url"
       dest: "{{ base_directory }}"
       checksum: "{{ cassandra[file_sha_cassandra] }}"
       timeout: 800
Atul6.Singh
  • 221
  • 2
  • 5