Dynamically generate bmc hostnames from static inventory?

Viewed 30

I have an ansible inventory that looks something like:

all:
  children:
    nodes:
      vars:
        bmc_user: root
        bmc_pass: secret
      hosts:
        node1.example.com:
          bmc_addr: node1-bmc.example.com
        node2.example.com:
          bmc_addr: node2-bmc.example.com

For running something like ipmitool, I can simply delegate the task to localhost, like this:

- hosts: all
  gather_facts: false
  tasks:
    - delegate_to: localhost
      command: >-
        ipmitool -I lanplus -H {{ bmc_addr }} -U {{ bmc_user }} -P {{ bmc_pass }} power off

Occasionally, I need to interact with the bmc using ssh. Thsi is tricky, because typically this requires password authentication. I want to write something like this:

- hosts: all
  gather_facts: false
  tasks:
    - delegate_to: "{{ bmc_addr }}"
      command: >-
        racadm set iDRAC.IPMILan.Enabled Enable

...but this won't work, because the credentials required to access the bmc are not the same as the credentials necessary to access the host. I would like to be able to do something like this in my inventory:

all:
  children:
    nodes:
      vars:
        bmc_user: root
        bmc_pass: secret
      hosts:
        node1.example.com:
          bmc_addr: node1-bmc.example.com
        node2.example.com:
          bmc_addr: node2-bmc.example.com
    nodes_bmc:
      vars:
        ansible_user: root
        ansible_ssh_pass: secret
      hosts:
        node1-bmc.example.com:
        node2-bmc.example.com:

...but I would like the hosts in the nodes_bmc group to be populated dynamically from the bmc_addr values in the nodes group, rather than having to specify them manually.

I've been puzzling over this for a bit, and the only solution I've come up with is to write some tooling that reads the ansible inventory and then generates a new static inventory containing the necessary entries. That seems clunky, and requires ensuring that the "generate bmc inventory" tool is run whenever the main inventory is modified.

2 Answers

Let's try the add_host module:

- name: add hosts
  hosts: all
  gather_facts: no
  tasks:
  - add_host:
      hostname: "{{ hostvars[item].bmc_addr }}"
      groups:
      - nodes_bmc
    loop: "{{ play_hosts }}"

Now, you need a new play so you have all the hosts in the play.

- name: Show all hosts
  hosts: all
  gather_facts: no
  tasks:
  - debug:
      var: play_hosts
    run_once: yes

Running with your inventory (but w/o any hosts in the node_bmc group):

$ ansible-playbook -i hosts.yml ./add_hosts.yml 

PLAY [add hosts] ***********************************************************************************************************

TASK [add_host] ************************************************************************************************************
ok: [node1.example.com] => (item=node1.example.com)
ok: [node1.example.com] => (item=node2.example.com)

PLAY [Show all hosts] ******************************************************************************************************

TASK [debug] ***************************************************************************************************************
ok: [node1-bmc.example.com] => {
    "play_hosts": [
        "node1-bmc.example.com",
        "node2-bmc.example.com",
        "node1.example.com",
        "node2.example.com"
    ]
}

PLAY RECAP *****************************************************************************************************************
node1-bmc.example.com      : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   
node1.example.com          : ok=1    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0   

Now, here's a totally different way.

You will have two file in your inventory:

  • The one you currently have (but w/o the hosts: section under nodes_bmc)
  • A generate_bmc.sh file, which must be executable: chmod +x generate_bmc.sh

That file will simply be this:

#!/bin/bash
# Figure out where we are
here=$( dirname $0 )

# Write header
cat << 'end_header'
{
  "nodes_bmc": {
    "hosts": [
end_header

# Get hosts from inventory file
hosts=$( ansible-inventory --list -i ${here}/hosts.yml | grep '"bmc_addr":' | cut -d: -f2 | sed 's/,$//' )
# Or, if you have jq installed:
# hosts=$( ansible-inventory --list -i ${here}/hosts.yml | jq "._meta.hostvars[].bmc_addr" )
last=$( echo $hosts | sed 's/.* //' )

# Write hosts
for host in ${hosts}
do
    echo -n "      "$host
    [ "${host}" != "${last}" ] && echo ","
done
echo

# Close output
cat << 'end_footer'
    ]
  }
}
end_footer

In your ansible.cfg file, have the line:

inventory = /the/directory/that/has/those/two/files

You can test this with ansible-inventory --list.

Related