Ansible - Parsing CSV List Of Hosts (IP, hostname(s), MAC)
Refreshed June 2026: when I first wrote this, parsing a CSV in Ansible meant a
filelookup plus a hand-written Jinja2 template. Thecommunity.general.read_csvmodule 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