Generate /etc/hosts, Archives and System Reports with Ansible

Use Ansible on Rocky Linux 10 to generate /etc/hosts from inventory and facts, create and extract archives, render system reports with Jinja2 templates, and fetch reports to the control node.

Published

Updated

Read time 12 min read

Reviewed byDeepak Prasad

Generate /etc/hosts, archives and system reports with Ansible template, archive and fetch on Rocky Linux 10

Ansible can turn inventory data and facts into files on managed hosts: render /etc/hosts from hostvars and groups, package directories with community.general.archive, extract bundles with ansible.builtin.unarchive, and publish HTML or text reports with ansible.builtin.template. This guide connects three admin workflows—generate files from inventory, archive or extract payloads, and collect facts for reports—on Rocky Linux 10.2 with playbooks you can rerun safely.

It assumes you can run plays from your first playbook and understand basic Jinja2 templates. Full DNS design, custom facts deep dives, and the complete copy/fetch tutorial are out of scope.

Tested on: Rocky Linux 10.2 (Red Quartz); kernel 6.12.0-211.16.1.el10_2.0.1.x86_64; ansible-core 2.16.16; community.general 9.5.2.

NOTE
This chapter is part of the GoLinuxCloud Ansible tutorial (RHCE EX294). Follow along from ~/ansible-project, inventory group lab, and playbooks in playbooks/. Use your own host names and paths if yours differ.
Task Module
Gather facts ansible.builtin.setup / gather_facts: true
Render /etc/hosts ansible.builtin.template
Create archive community.general.archive
Extract archive ansible.builtin.unarchive
Create report directory ansible.builtin.file
Fetch generated reports ansible.builtin.fetch
Verify generated file ansible.builtin.command or ansible.builtin.slurp
Data source Use when
ansible_host Network address for /etc/hosts (set in inventory for every host)
inventory_hostname Hostname column—name clients resolve
hostvars[host].ansible_default_ipv4.address Optional fallback only if ansible_host is missing—not recommended for /etc/hosts
groups['all'] You want every host in inventory
groups['lab'] You want only hosts in a selected group

What This Article Covers

  • Generating /etc/hosts from inventory network addresses (ansible_host) and hostnames only
  • Grouping hostnames that share the same ansible_host on one line
  • Deploying templates with backup: true
  • community.general.archive for tar.gz and other formats
  • ansible.builtin.unarchive with remote_src: true for on-host archives
  • ansible.builtin.setup / gather_facts for report data
  • Rendering system reports with Jinja2 and fetching them to the control node
Cover here Avoid here
/etc/hosts generation Full DNS design
hostvars, groups, inventory_hostname Full magic variables article
Facts for reports Full facts/custom facts article
template for generated files Full Jinja2/filter tutorial
archive / unarchive basics Backup strategy or disaster recovery
fetch generated reports Full file/copy/fetch article
Verification Full troubleshooting article

Why Generate Files and Reports with Ansible?

Manual edits and shell scripts Ansible modules
/etc/hosts drifts per server One template driven by inventory
Ad-hoc tar commands community.general.archive creates archives with predictable paths and module parameters
Reports copied host by host template + fetch in a playbook
No backup before overwrite backup: true on template

Generating files from inventory keeps lab and exam environments consistent—especially when EX294 objectives expect resolvable hostnames. Keeping logic in playbooks and thin templates matches idempotency practice. Use FQCN in playbooks—see modules and ansible-doc.


Prerequisites and Required Collections

  • Managed hosts running Rocky Linux 10 with Python for Ansible modules
  • ansible-core on the control node
  • Inventory with host names and connectivity (ansible_host when names differ from DNS)
  • community.general on the control node for archive
  • become: true when writing /etc/hosts, archives under /var, or system paths

Pin collections in requirements.yml:

yaml
---
collections:
  - name: community.general
    version: "9.5.2"
bash
ansible-galaxy collection install -r requirements.yml

Sample output:

output
Nothing to do. All requested collections are already installed.
bash
ansible lab -m ansible.builtin.ping

Sample output:

output
rocky1 | SUCCESS => {
    "changed": false,
    "ping": "pong"
}

Without community.general, archive tasks fail with couldn't resolve module/action 'community.general.archive'.


Key Modules Used in This Article

Module Role in this guide
ansible.builtin.setup Populates ansible_* facts and hostvars for templates
ansible.builtin.template Renders Jinja2 files such as /etc/hosts and reports
community.general.archive Creates compressed archives on the managed host
ansible.builtin.unarchive Extracts archives on the managed host
ansible.builtin.file Creates report or application directories
ansible.builtin.fetch Pulls generated reports to the control node

Official references appear in References at the end of this page.


Generate /etc/hosts from Inventory

/etc/hosts is for network name resolution only: map each unique IP address to one or more hostnames. It is not the place for OS facts, memory, or report data—those belong in separate report templates later in this guide.

The IP is the unique network identity on each line; hostnames are aliases clients resolve to that address.

Set ansible_host in inventory for every managed host—that value is the network address Ansible uses to connect, and it is what belongs in /etc/hosts. Do not pull OS or hardware facts into this file; discovered addresses such as ansible_default_ipv4 can differ from the address you intend in a lab or exam inventory.

Example inventory:

ini
[lab]
rocky1 ansible_host=10.0.2.15 ansible_connection=local
rocky2 ansible_host=10.0.2.15 ansible_connection=local
WARNING
Deploying /etc/hosts from Ansible is useful in lab and exam practice, but production hosts may regenerate or override that file—cloud-init, NetworkManager, DHCP hooks, or config-management agents can rewrite entries after your play. Always set backup: true on the template task so Ansible keeps a timestamped copy before replace. In production, coordinate with your platform team (DNS, DHCP reservations, or the sanctioned hosts-management tool) instead of treating a playbook as the only source of truth.

Deploy from inventory alone—no fact-gathering play required for /etc/hosts:

yaml
- name: Deploy /etc/hosts from inventory
  hosts: rocky1
  become: true
  gather_facts: false
  tasks:
    - name: Render /etc/hosts from inventory network addresses
      ansible.builtin.template:
        src: ../templates/hosts.j2
        dest: /etc/hosts
        owner: root
        group: root
        mode: '0644'
        backup: true
bash
ansible-playbook playbooks/deploy-hosts.yml

Sample output:

output
TASK [Render /etc/hosts from inventory network addresses] ***********************
changed: [rocky1]

A second run reports ok with changed: false when the template content is unchanged.


Use hostvars, groups and inventory_hostname

Inside a Jinja2 template, loop over a group, read each host's ansible_host, and group hostnames that share the same network address on one line:

jinja2
{% set ns = namespace(hosts_by_ip={}) %}
{% for host in groups['lab'] | sort %}
{% set ip = hostvars[host].ansible_host | default('') %}
{% if ip %}
{% if ip not in ns.hosts_by_ip %}
{% set _ = ns.hosts_by_ip.update({ip: []}) %}
{% endif %}
{% set _ = ns.hosts_by_ip[ip].append(host) %}
{% endif %}
{% endfor %}
{% for ip in ns.hosts_by_ip | sort %}
{{ ip }} {{ ns.hosts_by_ip[ip] | join(' ') }}
{% endfor %}
  • ansible_host is the only address column—inventory defines the network identity per host.
  • inventory_hostname is the name column—the hostname clients look up.
  • hostvars[host] reaches variables for hosts that are not the current task target.

See Ansible variables for naming rules; avoid variable names that collide with magic variables such as hostvars or groups.


Use Facts to Get IP Addresses and Host Details

For /etc/hosts, use inventory only: ansible_host plus inventory_hostname. That keeps the file aligned with how you designed connectivity—one network address per host, defined before the play runs.

Do not mix in ansible_default_ipv4, ansible_hostname, or other discovered facts for /etc/hosts. Those values come from gather_facts at runtime and may not match your inventory plan (multiple NICs, bridges, or lab hosts sharing one machine). Save facts for system reports later in this guide.

If a host truly has no ansible_host in inventory, fix the inventory rather than guessing from facts. A fact-based fallback is possible but discouraged:

jinja2
{% set ip = hostvars[host].ansible_host %}

That path requires a prior gather_facts play and is not the pattern used here.

For report templates, use richer ansible_* facts—see Ansible facts and custom facts.


Create a Jinja2 Template for /etc/hosts

Full templates/hosts.j2 from the lab:

jinja2
# {{ ansible_managed }}
127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
::1         localhost localhost.localdomain localhost6 localhost6.localdomain6

{% set ns = namespace(hosts_by_ip={}) %}
{% for host in groups['lab'] | sort %}
{% set ip = hostvars[host].ansible_host | default('') %}
{% if ip %}
{% if ip not in ns.hosts_by_ip %}
{% set _ = ns.hosts_by_ip.update({ip: []}) %}
{% endif %}
{% set _ = ns.hosts_by_ip[ip].append(host) %}
{% endif %}
{% endfor %}
{% for ip in ns.hosts_by_ip | sort %}
{{ ip }} {{ ns.hosts_by_ip[ip] | join(' ') }}
{% endfor %}

The ansible_managed comment marks the file as Ansible-controlled—useful when you audit changes later. Keep heavy logic in tasks (set_fact, vars) when templates grow beyond simple loops; see template module patterns.


Deploy /etc/hosts Safely with Backup

Always set backup: true on production /etc/hosts tasks:

yaml
- name: Render /etc/hosts from inventory and facts
  ansible.builtin.template:
    src: ../templates/hosts.j2
    dest: /etc/hosts
    backup: true

Ansible saves the previous file with a timestamp suffix before replacing it. Pair with owner, group, and mode: '0644' so permissions stay correct.


Verify Generated /etc/hosts

Inspect the rendered file on the target:

bash
tail -6 /etc/hosts

Sample output:

output
# Ansible managed
127.0.0.1   localhost localhost.localdomain localhost4 localhost4.localdomain4
::1         localhost localhost.localdomain localhost6 localhost6.localdomain6

10.0.2.15 rocky1 rocky2

One line per unique IP; multiple inventory hostnames on the same address become aliases on that line.

Confirm name resolution for an inventory host:

bash
getent hosts rocky2

Sample output:

output
10.0.2.15       rocky2

Inside a playbook, ansible.builtin.command with changed_when: false or ansible.builtin.slurp can capture file content for assertions—see debug playbook techniques.


Create Archives with community.general.archive

community.general.archive creates or replaces an archive on the managed host. The archive is not copied to the control node.

yaml
- name: Archive log directory as tar.gz
  community.general.archive:
    path: /var/log/ansible-demo
    dest: /tmp/ansible-demo-logs.tar.gz
    format: gz
    remove: false
bash
ansible-playbook playbooks/create-archive.yml

Sample output:

output
TASK [Archive log directory as tar.gz] *****************************************
changed: [rocky1]
bash
ls -lh /tmp/ansible-demo-logs.tar.gz

Sample output:

output
-rw-r--r--. 1 root root 231 Jul  9 14:59 /tmp/ansible-demo-logs.tar.gz

path accepts a directory, glob, or list. format supports gz, bz2, zip, tar, and others per the archive module documentation.

community.general.archive creates or replaces the archive on the managed host. If you need strict change reporting for backup workflows, test reruns with your source tree because archive metadata can still make tasks report changed.


Extract Archives with ansible.builtin.unarchive

When the archive already exists on the managed host, set remote_src: true:

yaml
- name: Ensure extraction directory exists
  ansible.builtin.file:
    path: /opt/demoapp
    state: directory
    mode: '0755'

- name: Extract archive on managed host
  ansible.builtin.unarchive:
    src: /tmp/ansible-demo-logs.tar.gz
    dest: /opt/demoapp
    remote_src: true
    creates: /opt/demoapp/ansible-demo/app.log
bash
ansible-playbook playbooks/extract-archive.yml

Sample output:

output
TASK [Ensure extraction directory exists] **************************************
ok: [rocky1]

TASK [Extract archive on managed host] *****************************************
changed: [rocky1]
bash
cat /opt/demoapp/ansible-demo/app.log

Sample output:

output
ansible archive demo log entry

Use creates: so extraction is skipped when the destination already exists—idempotent reruns stay ok.


archive vs unarchive vs fetch

Module Direction Typical use
community.general.archive Compress on managed host Package logs or config trees before backup
ansible.builtin.unarchive Expand on managed host Deploy application bundles
ansible.builtin.fetch Managed host → control node Pull generated reports or diagnostics

archive and unarchive operate on the remote system. fetch is the reverse of copy—use it after reports are rendered on each host. For everyday file copy workflows, see file, copy and fetch modules.


Collect System Facts with setup

gather_facts: true on a play (or ansible.builtin.setup as a task) runs the fact collector and fills ansible_* variables used in reports:

yaml
- name: Generate system reports from facts
  hosts: lab
  gather_facts: true
  tasks:
    - name: Render system report from facts
      ansible.builtin.template:
        src: ../templates/system_report.j2
        dest: "/var/reports/ansible/{{ inventory_hostname }}-report.txt"

Facts are per host—inventory_hostname, ansible_distribution, ansible_memtotal_mb, and similar keys are available inside the template without extra hostvars lookups on the same host.


Generate System Reports with Templates

Example templates/system_report.j2:

jinja2
# Ansible system report for {{ inventory_hostname }}

Hostname: {{ ansible_hostname }}
FQDN: {{ ansible_fqdn }}
OS: {{ ansible_distribution }} {{ ansible_distribution_version }}
Kernel: {{ ansible_kernel }}
Architecture: {{ ansible_architecture }}
Primary IPv4: {{ ansible_default_ipv4.address | default('n/a') }}
Memory MB: {{ ansible_memtotal_mb }}
Processor count: {{ ansible_processor_vcpus }}
bash
ansible-playbook playbooks/generate-reports.yml

Sample output:

output
TASK [Render system report from facts] *****************************************
changed: [rocky1]
changed: [rocky2]

Save Reports on Managed Hosts

Create the destination directory before templating:

yaml
- name: Ensure report directory exists
  ansible.builtin.file:
    path: /var/reports/ansible
    state: directory
    mode: '0755'

- name: Render system report from facts
  ansible.builtin.template:
    src: ../templates/system_report.j2
    dest: "/var/reports/ansible/{{ inventory_hostname }}-report.txt"
    mode: '0644'

Using {{ inventory_hostname }} in the filename avoids collisions when every host writes its own report locally.

bash
head -8 /var/reports/ansible/rocky1-report.txt

Sample output:

output
# Ansible system report for rocky1

Hostname: rocky1
FQDN: rocky1
OS: Rocky 10.2
Kernel: 6.12.0-211.16.1.el10_2.0.1.x86_64
Architecture: x86_64

Fetch Reports to the Control Node

ansible.builtin.fetch stores files in a hostname-based tree under dest:

yaml
- name: Fetch system report from managed host
  ansible.builtin.fetch:
    src: "/var/reports/ansible/{{ inventory_hostname }}-report.txt"
    dest: "{{ playbook_dir }}/../fetched-reports/"
    flat: false
    fail_on_missing: true
bash
ansible-playbook playbooks/fetch-reports.yml

Sample output:

output
TASK [Fetch system report from managed host] ***********************************
changed: [rocky1]
changed: [rocky2]

On the control node, files land under per-host paths:

output
fetched-reports/rocky1/var/reports/ansible/rocky1-report.txt
fetched-reports/rocky2/var/reports/ansible/rocky2-report.txt

Avoid flat: true when fetching the same basename from many hosts—later fetches overwrite earlier ones.


Common Examples

Generate /etc/hosts for all inventory hosts

jinja2
{% for host in groups['all'] | sort %}
{% if hostvars[host].ansible_host %}{{ hostvars[host].ansible_host }} {{ hostvars[host].inventory_hostname }}{% endif %}
{% endfor %}

Set ansible_host on every inventory host; no gather_facts required.

Map ansible_host to inventory_hostname

jinja2
{{ hostvars[host].ansible_host }} {{ hostvars[host].inventory_hostname }}

Create a tar.gz archive of a log directory

yaml
- name: Archive application logs
  community.general.archive:
    path: /var/log/myapp
    dest: /tmp/myapp-logs.tar.gz
    format: gz

Extract an application archive to a target directory

yaml
- name: Ensure extraction directory exists
  ansible.builtin.file:
    path: /opt/myapp
    state: directory
    mode: '0755'

- name: Deploy application bundle
  ansible.builtin.unarchive:
    src: /tmp/myapp-1.0.tar.gz
    dest: /opt/myapp
    remote_src: true

Generate a system report from facts

yaml
- name: Write fact-based report
  ansible.builtin.template:
    src: ../templates/system_report.j2
    dest: "/var/reports/ansible/{{ inventory_hostname }}-report.txt"

Fetch reports from all managed hosts

yaml
- name: Collect reports on control node
  hosts: lab
  tasks:
    - name: Fetch report file
      ansible.builtin.fetch:
        src: "/var/reports/ansible/{{ inventory_hostname }}-report.txt"
        dest: "{{ playbook_dir }}/../fetched-reports/"

Common Mistakes

Mistake Fix
Missing ansible_host in inventory Set the network address per host in inventory
Using facts in /etc/hosts Use ansible_host only; facts belong in reports
Overwriting /etc/hosts without backup Use backup: true on template
Generating hosts entries for the wrong group Loop over the intended group, such as groups['lab'], not always groups['all']
Creating archives without required collection Install community.general
Extracting into a missing directory Create dest with ansible.builtin.file before unarchive
Using unarchive without remote_src for on-host archives Set remote_src: true
Fetching reports with flat: true from many hosts Use per-host directories (flat: false)
Putting too much logic in templates Prepare variables in tasks when possible

Best Practices

Practice Why
Set ansible_host for every inventory host /etc/hosts uses inventory network addresses only
Keep /etc/hosts separate from fact-based reports Facts are for reports; inventory is for name-to-IP mapping
Pin community.general in requirements.yml archive is not in ansible-core
Use backup: true on /etc/hosts Roll back if a template error breaks resolution
Include ansible_managed in generated files Shows automation ownership
Use creates: on unarchive Idempotent extraction
Name reports with inventory_hostname Unique files per host
Fetch with flat: false Preserves hostname layout on control node
Keep templates thin Easier to test and review
Verify with getent hosts or slurp Confirms rendered content works

Summary

On Rocky Linux 10, generate /etc/hosts from inventory network addresses only: ansible_host on the left, inventory_hostname as aliases, one line per unique IP. Deploy with ansible.builtin.template and backup: true—no fact gathering required for the hosts file. Use gather_facts and ansible_* variables for system reports, not for /etc/hosts. Package directories with community.general.archive, extract with ansible.builtin.unarchive, and fetch reports with ansible.builtin.fetch.


References


Frequently Asked Questions

1. Why gather facts before rendering /etc/hosts for all hosts?

You do not need facts for /etc/hosts when every host has ansible_host set in inventory—that is the network address for the file. Gather facts only when you also build reports from ansible_* variables, or when a host lacks ansible_host and you choose a fact-based fallback.

2. Is community.general.archive part of ansible-core?

No. The archive module lives in community.general. Pin community.general 9.5.2 in requirements.yml for ansible-core 2.16.x and install with ansible-galaxy collection install -r requirements.yml.

3. When do I set remote_src: true on unarchive?

Set remote_src: true when the archive already exists on the managed host—for example /tmp/app.tar.gz created by a prior task. Without it, Ansible looks for the archive on the control node and copies it first.

4. Can fetch with flat: true collect reports from many hosts?

flat: true stores every file in the same directory using only the basename, so identical filenames from different hosts overwrite each other. Use flat: false so fetch creates per-host subdirectories under dest.

5. Should I overwrite /etc/hosts without a backup?

No. Use backup: true on ansible.builtin.template so Ansible keeps a timestamped copy of the previous /etc/hosts before replacing it.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive …