Case or if/else logic in 'when'

I want to install packages on several hosts.

now I have 3 lists in a defaults-file

packages_1:
  - vim
  - git

packages_2:
  - nano
  - less

packages_3:
  - zsh
  - go

my TASK would be:

- name: install packages to host 1
  package:
    name: "{{ item }}"
    state: present
  loop: "{{ packages_1}}"
  when: 1 in inventory_hostname

- name: install packages to host 2
  package:
    name: "{{ item }}"
    state: present
  loop: "{{ packages_2 }}"
  when: 2 in inventory_hostname

- name: install packages to host 3
  package:
    name: "{{ item }}"
    state: present
  loop: "{{ packages_3 }}"
  when: 3 in inventory_hostname


now, what I would like to achieve is to do this in a single task. This would require some sort of if/else or case logic.

Any idea, how I can achieve that?

Ansible isn’t very good at if/elif/else or case logic IMHO. I think this is by design, because it’s not a scripting language but an infrastructure-as-code tool.

That said, why do you define three lists in a defaults file? If the list of packages is unique to every host, why not have a packages variable for every host in your inventory? You would only need one task then and ansible would use the correct list of packages.

This would also make it clearer that those packages are specific for every host. Although it’s technically possible to do it your way, I think it’s better to have only default values in a defaults file. Everything else is confusing and looks like an accident waiting to happen :wink:

BTW you can have default variables and override them in several places. Although with 22 places where you can define a variable, precedence is a little bit complicated. Although in practice most people probably only use 3 or 4 places (like role defaults, inventory group_vars/all, inventory group_vars and inventory host_vars) where they define variables, which simplifies things.

3 Likes

thx for the pointers

not sure whether it makes any significant difference where to put such variants. Actually we are using a dynamic inventory, so there is no concrete way to place them where you suggest in my general setup.

I am aware of the variable precedence list and the challenges, opportunities it brings. And it’s true … once you build some sort of habbit you end up only using few locations out of the (almost) countless possibilities.

good to know that probaly looking for if/else kind of logic is, apparently. pointless.

Depending on how you’re generating your dynamic inventory you can create groups and vars. The servicenow, vmware, proxmox inventory plugins would be good examples to show you how to do this if your dynamic inventory plugin supports this. Otherwise, you could use some jinja and pare ii down to a single task. You can also add a hosts_vars and groups_vars in the same directory as your playbook. See: How to build your inventory — Ansible Community Documentation
Per the docs:
Ansible loads host and group variable files by searching paths relative to the inventory file or the playbook file

We don’t specify any group_vars or host_vars in our inventories. All our group_vars and host_vars are specific to projects. Maybe we’re just lucky that way, or not very imaginative. But to my mind, inventories define and group hosts; variables are a different thing and should be kept separate. I’d probably make an exception for connection variables, but they are sort of part of the host definition. Fortunately, Ansible’s defaults work for all our hosts, so we’ve avoided that distinction.

One option is to create a file for each list of packages in the vars-directory for the role.

$ tree vars/
vars/
├── package_1.yml
├── package_2.yml
└── package_3.yml
$ cat package_1.yml 
packages:
  - vim
  - git

This approach gives you the option to include the variables based on the inventory_hostname, and results in a single “install packages” task.

- name: Include host-specific variables.
  ansible.builtin.include_vars:
    file: "{{ role_path }}/vars/package_{{ inventory_hostname | split('.') | first }}.yml"

- name: Install packages.
  ansible.builtin.package:
    name: "{{ packages }}"
    state: present
2 Likes

When you find yourself looking for a case statement, the problem lends itself to an object lookup, like so:

vars:
  host_packages:
    '1':
      - vim
      - git
    '2':
      - nano
      - less
    '3':
      - zsh
      - go

# and then
tasks:
  - name: install packages to host
    ansible.builtin.package:
      name: "{{ item }}"
      state: present
    loop: "{{ host_packages[inventory_hostname] }}"


Note how I’m assuming inventory_hostname is a string, because it always will come as a string.

1 Like