How to use a dictionary of registered ansible variables in vars?

user3086551 picture user3086551 · May 22, 2018 · Viewed 13k times · Source

I want to pass multiple variables to a task using vars. Currently, I am doing it like below

vars:
    var1_name: "var1_value"
    var2_name: "var2_value"

As the number of variables can grow in size, I'd rather prefer to pass the dictionary of variables to the task using vars. I have constructed a dictionary of variables like below

- name: set fact
  hosts: localhost
  tasks:
  - set_fact:
      variables: "{{ variables|default({}) | combine( {item.variable: item.value} ) }}"
    with_items:
    - variable: var1_name
      value: "var1_value"
    - variable: var2_name
      value: "var2_name"

Dictionary looks something like this:

"variables": {
    "var1_name": "var1_value",
    "var2_name": "var2_value",
}

Now, I want to make variables in this dictionary available to roles executing on other hosts.

But, when I tried to pass dictionary to vars like below

vars: "{{ variables }}"

Ansible throws the error:

ERROR! Vars in a Play must be specified as a dictionary, or a list of dictionaries

How to pass a dictionary variable in vars?

Answer

1efty picture 1efty · Jun 3, 2018

After doing some searching through the Ansible source code, it looks like this is an issue even the developers of Ansible face. In some integration tests, there are specific tests that are commented out because of this same error.

ansible/tests/integration/targets/include_import/role/test_include_role.yml

    ## FIXME Currently failing with
    ## ERROR! Vars in a IncludeRole must be specified as a dictionary, or a list of dictionaries
    # - name: Pass all variables in a variable to role
    #   include_role:
    #     name: role1
    #     tasks_from: vartest.yml
    #   vars: "{{ role_vars }}"

I also found out that this is the underlying function that is being called to include the variables:

def _load_vars(self, attr, ds):
        '''
        Vars in a play can be specified either as a dictionary directly, or
        as a list of dictionaries. If the later, this method will turn the
        list into a single dictionary.
        '''

        def _validate_variable_keys(ds):
            for key in ds:
                if not isidentifier(key):
                    raise TypeError("'%s' is not a valid variable name" % key)

        try:
            if isinstance(ds, dict):
                _validate_variable_keys(ds)
                return combine_vars(self.vars, ds)
            elif isinstance(ds, list):
                all_vars = self.vars
                for item in ds:
                    if not isinstance(item, dict):
                        raise ValueError
                    _validate_variable_keys(item)
                    all_vars = combine_vars(all_vars, item)
                return all_vars
            elif ds is None:
                return {}
            else:
                raise ValueError
        except ValueError as e:
            raise AnsibleParserError("Vars in a %s must be specified as a dictionary, or a list of dictionaries" % self.__class__.__name__,
                                     obj=ds, orig_exc=e)
        except TypeError as e:
            raise AnsibleParserError("Invalid variable name in vars specified for %s: %s" % (self.__class__.__name__, e), obj=ds, orig_exc=e)

Seems as if since "{{ }}" is actually just a YAML string, Ansible doesn't recognize it as a dict, meaning that the vars attribute isn't being passed through the Jinja2 engine but instead being evaluated for what it actually is.

The only way to pass YAML objects around would be to use anchors, however this would require that the object be defined in whole instead of dynamically.

var: &_anchored_var 
  attr1: "test"
  att2: "bar"

vars:
  <<: *_anchored_var