Set connection variables based on variable value in inventory before attempting to connect to host?

66 Views Asked by At

I have an inventory of mixed RHEL and Windows hosts requiring different Connection variable values. These hosts are sorted into a parent group (their team) and child group (their app).

I could have them in multiple groups: "RHEL"/"Windows" and their team. But I'd like to have them listed in my inventory once. I'd also like to avoid defining the same set of connection variables for every RHEL host or Windows host at the host level. There's one set of variables for rhel hosts, and one set of variables for windows hosts. I do not want to create child groups for the OS because I already have child groups for the host's app.

Can I simply set a variable (os: rhel, or os: windows) at the host level, and then later assign the connection variables based on the value of this os variable? This would need to be done before ansible tries to connect to the host.

So the logic would look something like this I think?:

  1. ansible reads inventory hosts based on host pattern
  2. prepare to execute on host1
  3. read os value on host1
  4. assign ansible connection variables for host1 based on os value for host1
  5. connect to host1 using these newly assigned variables
  6. execute playbook on host1

If I can do something like this, how?

edit:

A truncated example of the inventory I think I want to use:

---
TeamOne:
  vars:
    team:
      name: <specific team named as set in a tool we use>
      id: <id>
    assignment_group: <value>
  children:
    AppOne:
      hosts:
        hostname1:
          os: rhel
          ansible_host: <ip address>
        hostname2:
          os: windows
          ansible_host: <ip_address>
      vars:
        tags:
          application_appid: <appid>
          application_name: <appname>
    AppTwo:
      hosts:
        hostname3:
          os: rhel
          ansible_host: <ip address>
        hostname4:
          os: windows
          ansible_host: <ip_address>
      vars:
        tags:
          application_appid: <appid>
          application_name: <appname>     
FoobarTeam:
  vars:
    team:
      name: <specific team named as set in a tool we use>
      id: <id>
    assignment_group: <value>
  children:
    FooApp:
      hosts:
        foohost1:
          os: rhel
          ansible_host: <ip address>
        foohost2:
          os: windows
          ansible_host: <ip_address>
      vars:
        tags:
          application_appid: <appid>
          application_name: <appname>
    BarApp:
      hosts:
        barhost1:
          os: rhel
          ansible_host: <ip address>
        barhost2:
          os: windows
          ansible_host: <ip_address>
      vars:
        tags:
          application_appid: <appid>
          application_name: <appname>

Now multiply that by about 20 (and growing) teams, each with a varying number of apps under them as child groups, with any number of hosts (some as low as 2, others as high as 50 or more). We're at 250 hosts and planning to have to onboard more.

3

There are 3 best solutions below

2
inspiris On

I did the following in my playbook to accomplish this. The key points are to disable gather_facts at the play level, set your ansible connection vars using set_facts based on the variable in your inventory, then manually run gather_facts with the setup module. After that you can write the rest of your play.

- name: playbook name
  hosts: TeamGroupName
  gather_facts: no
  tasks:
    - name: If os is RHEL, assign RHEL connection variables
      when: os == 'rhel'
      set_fact:
        ansible_connection: ssh
        ansible_become_method: dzdo
    - name: If OS is Windows, assign Windows connection variables
      when: os == 'windows'
      set_fact:
        ansible_connection: winrm
        ansible_port: 5985
    - name: Gathering facts
      setup:
    ...rest of the playbook can now be added here...
11
Alexander Pletnev On

I decided to make my answer more structural and list several ideas.

Options that require changes to the inventory

Use the dynamic inventory

Since you use ServiceNow as CMDB, you may want to check out servicenow.itsm collection that includes an inventory plugin. Using it, you would be able to retrieve the parameters from its tables.

It could also be used in combination with your existing static one.

If you experience issues with the inventory plugin itself (e.g. you have custom CMBD setup with custom tables and CI relations), you can use the API modules of that collection.

Define the separate groups for Windows and RHEL hosts

I could have them in multiple groups: "RHEL"/"Windows" and their team. But I'd like to have them listed in my inventory once. I'd also like to avoid defining the same set of connection variables for every RHEL host or Windows host at the host level. There's one set of variables for rhel hosts, and one set of variables for windows hosts. I do not want to create child groups for the OS because I already have child groups for the host's app.

I understand your preference but this looks like the simplest and the most correct thing to do. You don't need to set the connection variables per host - if you have groups windows and rhel, you can set ansible_connection as a group variable. Your inventory file could look like this:

# inventory-canonic.yaml
---
all:
  children:
    some_group:
      hosts:
        win[1:3]:
        rhel[1:3]:
    another_group:
      hosts:
        win[4:5]:
        rhel[4:5]:
    windows:
      hosts:
        win[1:5]:
      vars:
        ansible_connection: winrm
    rhel:
      hosts:
        rhel[1:5]:
      vars:
        ansible_connection: ssh

Can I simply set a variable (os: rhel, or os: windows) at the host level

Technically you can, but there's no need to: as the example above shows, Ansible has a special variable specifically for this. But setting it on host level is not a scalable approach.

Refactor the existing inventory

If you have a large inventory, consider splitting it into several smaller ones - they will be easier to maintain and you can set the common parameters as another separate inventory or playbook group vars. Also they would be safer to use as you will no longer target all the hosts but only ones that should be affected. It is usually recommended to split the hosts by the environment. I don't know if you use this approach, but regardless to that, distinction by the team name is also a good option. You can also use different structures:

inventories/
├── common
│   ├── group_vars
│   │   ├── rhel.yaml
│   │   └── windows.yaml
│   └── hosts.yaml
├── FoobarTeam
│   ├── dev
│   │   └── hosts.yaml
│   ├── prod
│   │   └── hosts.yaml
│   └── uat
│       └── hosts.yaml
└── TeamOne
    ├── AppOne.yaml
    └── AppTwo.yaml

Options that don't depend on the inventory

So the logic would look something like this I think?:

  1. ansible reads inventory hosts based on host pattern

This is possible, but there are more flexible ways to achieve the same without changing the inventory.

SSH on Windows

First of all, Ansible supports SSH on Windows. But the documentation states that this is still an experimental feature as of ansible-core 2.16, and there are manual actions that should be performed beforehand. If it's not a problem in your environment, then the most straightforward solution is to connect to all servers using SSH, and gather facts to handle the further differences.

Attempting to detect the connection method automatically

Also, you can try to detect the connection type automatically using try-catch logic provided by block. I wouldn't say I like this implementation - for example, it would rely on reassigning the variables as facts. But I think it should work, and this playbook could be then imported into any other playbook (or the logic could be moved in a role):

# playbook-autodetect-connection-type.yaml
---
- name: Detect the connection type
  hosts: all
  gather_facts: false
  tasks:
    - name: Try to connect using WinRM first, then fall back to SSH
      block:
        - name: Try to connect using WinRM
          vars:
            ansible_connection: winrm
            ansible_port: 5985
          block:
            - name: Gather facts using WinRM
              setup:

            - name: Set the corresponding connection parameters for WinRM
              set_fact:
                ansible_connection: "{{ ansible_connection }}"
                ansible_port: "{{ ansible_port }}"          
      rescue:
        - name: Try to connect using SSH
          vars:
            ansible_connection: ssh
            ansible_become_method: dzdo
          block:
            - name: Gather facts using SSH
              setup:

            - name: Set the corresponding connection parameters for SSH
              set_fact:
                ansible_connection: "{{ ansible_connection }}"
                ansible_become_method: "{{ ansible_become_method }}"
      always:
        - name: Display the detected connection type
          debug:
            var: ansible_connection

Options that depend on the inventory

Setting the connection method based on the host name

These options would require your host names to be defined in a pattern that allows to distinct them by the OS.

Ternary filter for simple cases

The simplest option would cover the case when you only have two possible types of connections:

# inventory.yaml
---
all:
  children:
    some_group:
      hosts:
        win[1:3]:
        rhel[1:3]:
    another_group:
      hosts:
        win[4:5]:
        rhel[4:5]:
# playbook-simplest.yaml
---
- name: Setup the hosts
  hosts: all
  connection: "{{ inventory_hostname.startswith('win') | ternary('winrm', 'ssh') }}"
  tasks:
    - debug:
        var: ansible_connection

Dynamic creation of groups for more complex inventory

If you have more OS options, inline Jinja templates will become harder to develop and maintain. A better option could be to iterate over the list of hosts in the inventory and set the ansible_connection variable based on the host name dynamically. This approach will also give you an opportunity to target the hosts based on their OS in the subsequent plays. With the same inventory as in the previous example, the playbook would look like this:

# playbook.yaml
---
- name: Detect the connection type
  hosts: all
  connection: local
  gather_facts: false
  tasks:
    - name: Add host to Windows group
      add_host:
        name: "{{ hostvars[item].inventory_hostname }}"
        group: windows
        ansible_connection: winrm
      when: hostvars[item].inventory_hostname.startswith('win')
      loop: "{{ ansible_play_hosts_all }}"

    - name: Add host to RHEL group
      add_host:
        name: "{{ hostvars[item].inventory_hostname }}"
        group: rhel
        ansible_connection: ssh
      when: hostvars[item].inventory_hostname.startswith('rhel')
      loop: "{{ ansible_play_hosts_all | difference(groups['windows']) }}"

    # More conditions could be defined further

- name: Further actions
  hosts: all
  tasks:
    - name: Further tasks go here
      debug:
        var: ansible_connection
2
Marek Opatrny On

delegate_to with when clause would do the trick.

Example playbook:

- hosts: all
  gather_facts: false
  roles:
    - awesome_role

Inventory

all:
  children:
    servers:
      hosts:
        server01:
          ansible_host: 1.2.3.4
        server02:
          ansible_host: 4.3.2.1

Awesome_role

└── awesome_role
    └── tasks
        ├── main.yml
        ├── rhel.yml
        └── windows.yml



$ cat awesome_role/tasks/main.yml 
- name: Set Win host
  ansible.builtin.import_tasks: windows.yml
  delegate_to: "{{ inventory_hostname }}"
  delegate_facts: true
  vars:
    ansible_user: admin

- name: Set RHEL host
  ansible.builtin.import_tasks: rhel.yml
  delegate_to: "{{ inventory_hostname }}"
  delegate_facts: true
  vars:
    ansible_user: root
    ansible_port: 2222

$ cat awesome_role/tasks/rhel.yml
- name: Gather facts
  ansible.builtin.setup:

- name: Create file
  ansible.builtin.file:
    state: touch
    path: /rhelfile
    

$ cat awesome_role/tasks/windows.yml 
- name: Create file
  ansible.builtin.file:
    state: touch
    path: /windowsfile