Using Set Fact from hosts.yaml json format

I need to create a workflow where I have a “host” device where it has a key value of “SN”

I need to use passed playbook “extra var” input , and using that set fact SN that I can use in later tasks. Seems simple just reuturn key / value from hosts.. but turns out it is not working. I have tried many ways.

command:
ansible-playbook -i inventories/production/hosts.yaml --limit triton tabletop-deploy.yaml --user ansible -e tabletop_number=“$tabletop_number”

tasks:

- name: Search ansible hosts and set fact based on SN
  set_fact:
    sn_value: "{{ hostvars[item].SN }}"
  loop: "{{ groups['arduino_boards'] | list }}"
  when: item == tabletop_number
  register: sn_result

- name: Print SN value
  debug:
    msg: "SN value: {{ sn_result.results.ansible_facts.sn_value }}"

Example hosts.yaml

all:
  children:
    pi_servers:
      hosts:
        triton:
          ansible_ssh_common_args: '-o StrictHostKeyChecking=no'
          # ssh_pub_key: # ssh_key_pub in SCM
          ssid: penguinpages
          wifi_password: $PASSWORD # Set in SCM
        baclava:
          ansible_ssh_common_args: '-o StrictHostKeyChecking=no'
    arduino_boards:
      hosts:
        t01:
          SN: 24EC4A2365A4
          ip_address: 172.16.100.121
          ansible_ssh_common_args: '-o StrictHostKeyChecking=no'
        t02:
          SN: 48CA435CF294
          ip_address: 172.16.100.122
          ansible_ssh_common_args: '-o StrictHostKeyChecking=no'
        t03:
          SN: 1234567
          ip_address: 172.16.100.123
          ansible_ssh_common_args: '-o StrictHostKeyChecking=no'

I would expect output to be if input was “t02” then SN fact set would be SN=48CA435CF294

But I have tried many many ways and keep getting back bad results.

Error:

<triton> SSH: EXEC ssh -vvv -C -o ControlMaster=auto -o ControlPersist=60s -o KbdInteractiveAuthentication=no -o PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey -o PasswordAuthentication=no -o 'User="ansible"' -o ConnectTimeout=10 -o StrictHostKeyChecking=no -o 'ControlPath="/root/.ansible/cp/0901649b71"' triton '/bin/sh -c '"'"'rm -f -r /home/ansible/.ansible/tmp/ansible-tmp-1745329422.0644264-70-103872123862905/ > /dev/null 2>&1 && sleep 0'"'"''
<triton> (0, b'', b"OpenSSH_9.9p2, OpenSSL 3.3.3 11 Feb 2025\r\ndebug1: Reading configuration data /etc/ssh/ssh_config\r\ndebug1: /etc/ssh/ssh_config line 22: include /etc/ssh/ssh_config.d/*.conf matched no files\r\ndebug3: expanded UserKnownHostsFile '~/.ssh/known_hosts' -> '/root/.ssh/known_hosts'\r\ndebug3: expanded UserKnownHostsFile '~/.ssh/known_hosts2' -> '/root/.ssh/known_hosts2'\r\ndebug1: auto-mux: Trying existing master at '/root/.ansible/cp/0901649b71'\r\ndebug2: fd 3 setting O_NONBLOCK\r\ndebug2: mux_client_hello_exchange: master version 4\r\ndebug3: mux_client_forwards: request forwardings: 0 local, 0 remote\r\ndebug3: mux_client_request_session: entering\r\ndebug3: mux_client_request_alive: entering\r\ndebug3: mux_client_request_alive: done pid = 58\r\ndebug3: mux_client_request_session: session request sent\r\ndebug1: mux_client_request_session: master session id: 2\r\ndebug3: mux_client_read_packet_timeout: read header failed: Broken pipe\r\ndebug2: Received exit status from master 0\r\n")
ok: [triton]
TASK [serial-scan : Set Ansible facts for shell variables] *********************
task path: /builds/penguinpages/infra/shuffleboard/roles/serial-scan/tasks/main.yaml:1
ok: [triton] => {
    "ansible_facts": {
        "tabletop_number": "t01"
    },
    "changed": false
}
TASK [serial-scan : Output Ansible facts] **************************************
task path: /builds/penguinpages/infra/shuffleboard/roles/serial-scan/tasks/main.yaml:9
ok: [triton] => {
    "msg": [
        "tabletop_number: t01"
    ]
}
TASK [serial-scan : Search ansible hosts and set fact based on SN] *************
task path: /builds/penguinpages/infra/shuffleboard/roles/serial-scan/tasks/main.yaml:18
ok: [triton] => (item=t01) => {
    "ansible_facts": {
        "sn_value": "24EC4A2365A4"
    },
    "ansible_loop_var": "item",
    "changed": false,
    "item": "t01"
}
skipping: [triton] => (item=t02)  => {
    "ansible_loop_var": "item",
    "changed": false,
    "false_condition": "item == tabletop_number",
    "item": "t02",
    "skip_reason": "Conditional result was False"
}
skipping: [triton] => (item=t03)  => {
    "ansible_loop_var": "item",
    "changed": false,
    "false_condition": "item == tabletop_number",
    "item": "t03",
    "skip_reason": "Conditional result was False"
}
<snip>
}
TASK [serial-scan : Print SN value] ********************************************
task path: /builds/penguinpages/infra/shuffleboard/roles/serial-scan/tasks/main.yaml:25
fatal: [triton]: FAILED! => {
    "msg": "The task includes an option with an undefined variable.. 'list object' has no attribute 'ansible_facts'\n\nThe error appears to be in '/builds/penguinpages/infra/shuffleboard/roles/serial-scan/tasks/main.yaml': line 25, column 3, but may\nbe elsewhere in the file depending on the exact syntax problem.\n\nThe offending line appears to be:\n\n\n- name: Print SN value\n  ^ here\n"
}
PLAY RECAP *********************************************************************
triton                     : ok=4    changed=0    unreachable=0    failed=1    skipped=0    rescued=0    ignored=0 

Its like it loops through “hosts” and finds the value "“sn_value”: “24EC4A2365A4” but never can return it as a fact or usable. I have tried dozens of means to get this done and nothing can end in "input = “hostname” and return a sub fact under that host of field “SN” set as a fact.

You don’t need sn_result.results.ansible_facts.sn_value. It’s not valid var name. Try this:

- name: Debug
  ansible.builtin.debug:
      msg: " SN value: {{ sn_value }}"

Edit: Grammar

Thanks for response.. I agree that the value should be simple call value “SN”

But not sure I track how your code example will help. Ex: if extra var passed is “tabletop_number=t02” then it should return fact “SN: 48CA435CF294” and as important I need ot set the fact so later on I can make a call like next stage:

- name: Run arduino-cli board list command
  command: arduino-cli board list --format json
  register: arduino_cli_output

- name: Set Arduino CLI output as a fact
  set_fact:
    arduino_cli_data: "{{ arduino_cli_output.stdout | from_json }}"

- name: Find matching serial number and set address
  set_fact:
    address: "{{ item.port.address }}"
  loop: "{{ arduino_cli_data.detected_ports }}"
  when: item.port.properties is defined and item.port.properties.serialNumber is defined and item.port.properties.serialNumber == tabletop_serial

- name: Output address fact address which is TTY port for {{ tabletop_number }}
  debug:
    msg:
      - "{{ tabletop_number }}"
      - "{{ tabletop_serial }}"
      - "{{ board_id }}"
      - "{{ address }}"

because I have to use key value of SN to then learn what TTY port the device is on to flash firmware to board.

You don’t need to loop your inventory hosts to get your data. Since you’re specifying the inventory file and using limit. The hosts and vars from the inventory are still availalble to you:

So I have to make some assumptions since I don’t see your full playbook, but I am using the extra_vars passed at run time

- hosts: all
  gather_facts: true
  become: true
  tasks:
    
     # You don't need to loop to set a fact here.  You can just set a fact or call it natively
    - name:  Print the parameters of the defined tabletop_number value
      ansible.buiitin.debug:
          msg: 
           - "{{ hostvars['tabletop_number']['SN'] }}"
           - "{{ hostvars['tabletop_number']['ip_address'] }}"

Now since I don’t know what you’re data from the cli output is, but assuming it’s a dictiionary, I would then use selectattr and map to get the board_id when the tabletop_serial number matches a key value that you’re looking for.

Maybe these two articles can help you towards your goals>

1 Like

At risks of answering your question without answering your question; the intention of this work is because you need to know which Linux device maps to which Arduino when more than one is plugged in.

Thankfully we have persistent device names and udev in Linux. Out of the box you can reliably and persistently identify USB devices by their path. Look inside /dev/serial/by-path to understand what devices are available. While convenient this may not work with Arduino tooling specifically; historically there were bugs where it expected certain device names. We can rely on UDEV rules to have symbolic links created when a device is inserted by fields like serial number.

Here I map a specific Arduino Uno R3 by serial number to /dev/ttyACM89 by placing an entry in a file called /etc/udev/rules.d/99-arduino.rules:

SUBSYSTEM=="usb", ATTR{serial}=="88354323435351F001B2", SYMLINK+="ttyACM89"

In the line of the actual question; if a host in the inventory has an inventory_hostname of t02 you can access it at any point by using the hostvars magic variable (dictionary) by hostvars["t02"].SN or:

- ansible.builtin.debug:
    msg: >-
      The serial number for board {{ b }} is {{ hostvars[b].SN }}
  vars:
    b: t02

If you want to persist discovery information about a different host in the inventory and saving it that is when you use delegate_to and delegate_facts: true like:

- ansible.builtin.set_fact:
    tty: /dev/ttyACM0
  delegate_to: t02
  delegate_facts: true
1 Like

Two things

  1. Never thought of idea to use udev to set hard path for tty. Great idea for my use case

  2. My issue with using vars I think was not just format, but also idea that I could use them in other tasks as direct variables. I first have to set as fact the item.

Ex: (fails)

- name: Set Arduino CLI output as a fact
  set_fact:
    arduino_cli_data: "{{ arduino_cli_output.stdout | from_json }}"

- name: Find matching serial number and set address
  set_fact:
    address: "{{ item.port.address }}"
  loop: "{{ arduino_cli_data.detected_ports }}"
  when: item.port.properties is defined and item.port.properties.serialNumber is defined and item.port.properties.serialNumber == {{ hostvars[tabletop_number].ip_address }}

- name: Output address fact address which is TTY port for {{ tabletop_number }}
  debug:
    msg:
      - "{{ address }}"

but if I set fact first.. it works

- name: Set Ansible facts
  ansible.builtin.set_fact:
	tabletop_number: "{{ lookup('env', 'tabletop_number') | string }}"
    tabletop_number: "{{ lookup('env', 'tabletop_number') | string }}"
    tabletop_serial: "{{ hostvars[tabletop_number].SN }}"

- name: Run arduino-cli board list command
  command: arduino-cli board list --format json
  register: arduino_cli_output

- name: Set Arduino CLI output as a fact
  set_fact:
    arduino_cli_data: "{{ arduino_cli_output.stdout | from_json }}"

- name: Find matching serial number and set address
  set_fact:
    address: "{{ item.port.address }}"
  loop: "{{ arduino_cli_data.detected_ports }}"
  when: item.port.properties is defined and item.port.properties.serialNumber is defined and item.port.properties.serialNumber == tabletop_serial

- name: Output address fact address which is TTY port for {{ tabletop_number }}
  debug:
    msg:
	  - "{{ tabletop_number }}"
      - "{{ tabletop_serial }}"
      - "{{ address }}"

Thanks for help

Without seeing youor arduino_cli_output, it’s hard to provide a thorough answer. However, based on my experience, using the set_fact from loop may be unneccessary. You should be able to retrieve that data using selectattr or select filters depending on the data structure without looping and without having to set_fact. Even if you use set_fact to save keystrokes, you can still probably get away without having to loop.