Ansible - Parsing CSV List Of Hosts (IP, hostname(s), MAC)

3 minute read

Refreshed June 2026: when I first wrote this, parsing a CSV in Ansible meant a file lookup plus a hand-written Jinja2 template. The community.general.read_csv module does the whole thing in one task now, so that is the approach I lead with here. The original technique is still at the bottom for reference. Output below is from ansible-core 2.19 with community.general 13.

Background

I had a spreadsheet full of hosts: hostnames, a generic inventory name, IP addresses, and MAC addresses. I wanted it as structured data Ansible could consume. Back in 2017 I reached for the csvfile lookup, found it too rigid, and ended up reading the file as raw text and splitting it apart in a Jinja2 template. It worked, but it was more moving parts than the job needed.

You almost never need that today. community.general.read_csv reads a CSV straight into a list of dictionaries keyed by the header row.

The Sample CSV

A small version of the spreadsheet, exported to hosts.csv. The first row is the header, and those column names become the dictionary keys.

inventory_hostname,inventory_name,ansible_host,macaddress
hamlin-brenda.etsbv.internal,server000,10.14.200.90,02:16:3e:56:a0:f6
sicilian-michael.etsbv.internal,server001,10.183.84.9,02:16:3e:27:9c:64
oldham-ethel.etsbv.internal,server002,10.175.134.46,02:16:3e:6b:96:56

The Modern Approach: read_csv

One task reads the file, one writes it back out as YAML if you want a file on disk. If you only need the data in the current run, the read task alone is enough.

---
- name: Parse a CSV of hosts into usable Ansible data
  hosts: localhost
  connection: local
  gather_facts: false
  become: false
  tasks:
    - name: Read hosts.csv into a list of dictionaries
      community.general.read_csv:
        path: hosts.csv
      register: csv_hosts
      run_once: true

    - name: Show the parsed records
      ansible.builtin.debug:
        var: csv_hosts.list

    - name: Write the parsed data out to YAML
      ansible.builtin.copy:
        content: "{{ {'hostipmacs': csv_hosts.list} | to_nice_yaml }}"
        dest: ./parsed_hosts.yml

Running it:

ansible-playbook parse_csv.yml
TASK [Read hosts.csv into a list of dictionaries] ******************************
ok: [localhost]

TASK [Show the parsed records] *************************************************
ok: [localhost] => {
    "csv_hosts.list": [
        {
            "ansible_host": "10.14.200.90",
            "inventory_hostname": "hamlin-brenda.etsbv.internal",
            "inventory_name": "server000",
            "macaddress": "02:16:3e:56:a0:f6"
        },
        {
            "ansible_host": "10.183.84.9",
            "inventory_hostname": "sicilian-michael.etsbv.internal",
            "inventory_name": "server001",
            "macaddress": "02:16:3e:27:9c:64"
        },
        {
            "ansible_host": "10.175.134.46",
            "inventory_hostname": "oldham-ethel.etsbv.internal",
            "inventory_name": "server002",
            "macaddress": "02:16:3e:6b:96:56"
        }
    ]
}

And the YAML the third task writes to parsed_hosts.yml:

hostipmacs:
-   ansible_host: 10.14.200.90
    inventory_hostname: hamlin-brenda.etsbv.internal
    inventory_name: server000
    macaddress: 02:16:3e:56:a0:f6
-   ansible_host: 10.183.84.9
    inventory_hostname: sicilian-michael.etsbv.internal
    inventory_name: server001
    macaddress: 02:16:3e:27:9c:64
-   ansible_host: 10.175.134.46
    inventory_hostname: oldham-ethel.etsbv.internal
    inventory_name: server002
    macaddress: 02:16:3e:6b:96:56

No string splitting, no template, and the column headers carry through as keys.

One thing worth knowing: do not register the result to a variable named hosts. That is a reserved name in current ansible-core and you will get a warning. I named it csv_hosts above for exactly that reason.

read_csv also takes a fieldnames option if your file has no header row, and a delimiter option for tab or pipe separated files. See the module docs.

The Original Approach (For Reference)

If you are on a stripped-down control node where you cannot install community.general, you can still do this with built-ins only: read the file as text and build the structure in a template. This is what the 2017 version of this post used, modernized to FQCN.

Playbook:

---
- name: Parse CSV with a file lookup and template
  hosts: localhost
  connection: local
  gather_facts: false
  become: false
  vars:
    csvfile: "{{ lookup('file', 'hosts.csv') }}"
  tasks:
    - name: Parse CSV To YAML
      ansible.builtin.template:
        src: ./iterate_csv.j2
        dest: ./iterate_hosts.yml
      run_once: true

Template (iterate_csv.j2):

hostipmacs:
{% for item in csvfile.split("\n") %}
{%   if loop.index != 1 and item | trim != "" %}
{%     set fields = item.split(",") %}
  - host: '{{ fields[0] | trim }}'
    inventory_name: '{{ fields[1] | trim }}'
    ip: '{{ fields[2] | trim }}'
    mac: '{{ fields[3] | trim }}'
{%   endif %}
{% endfor %}

It gets the same result, just with more to maintain and more places for a malformed row to bite you. Reach for it only when read_csv is genuinely not available.

If you want a quick way to generate a large random test CSV like the 1000-host file I used originally, the old Python generator is still in this gist. It is Python 2 era and depends on the names library, so treat it as a starting point rather than something to run as-is.

Conclusion

The spreadsheet-to-Ansible problem has not gone away, but the tooling got a lot better. For anything routine, community.general.read_csv is one task and you are done.

Enjoy!

Comments