Ansible Jinja2 Templates for Network Device Configs - 夜莺博客

Ansible Jinja2 Templates for Network Device Configs

The difference between a script that pushes commands and an automation platform that people trust is the data model: VLANs, uplinks, NTP servers and access lists live in variables, and a Jinja2 template turns those variables into vendor syntax. Done properly, onboarding a new switch means filling in a host variable file and running a playbook — with a diff reviewed before anything is committed. Here is the full pattern: inventory, facts, templates, role layout and the safety habits that keep changes reversible.

Inventory: network_cli, Not SSH

Network devices do not run Python, so Ansible must connect in network mode. The essentials in your inventory group vars:

[switches:children]
eos
ios

[eos]
eos01.example.net

[ios]
ios01.example.net

[all:vars]
ansible_connection=ansible.netcommon.network_cli
ansible_user=automation
ansible_password={{ vault_switch_password }}
ansible_become=yes
ansible_become_method=enable

[eos:vars]
ansible_network_os=arista.eos.eos

[ios:vars]
ansible_network_os=cisco.ios.ios

ansible_network_os is mandatory with network_cli: it tells Ansible which platform module set to use. Without network_cli, Ansible tries to run Python over SSH on the switch and fails immediately — the single most common first-time error.

Facts Come Back Under ansible_net_*

Gather facts before generating configuration, and you can build templates that adapt to the device instead of assuming:

- name: collect facts
  hosts: switches
  gather_facts: false
  tasks:
    - name: gather network facts
      ansible.netcommon.cli_facts:
      register: cli

    - name: write facts to a report
      ansible.builtin.copy:
        dest: "./reports/{{ inventory_hostname }}.txt"
        content: |
          #jinja2: lstrip_blocks: True
          hostname: {{ ansible_net_hostname }}
          model:    {{ ansible_net_model }}
          version:  {{ ansible_net_version }}
          serial:   {{ ansible_net_serialnum }}

Facts such as ansible_net_hostname, ansible_net_model, ansible_net_version and ansible_net_interfaces are what let one template serve a whole fleet.

Variables: The Source of Truth

# group_vars/switches.yml
ntp_servers:
  - 10.10.10.5
  - 10.10.10.6
syslog_servers:
  - 10.10.10.20

vlans:
  - { id: 10,  name: users,   subnet: 10.20.10.0/24 }
  - { id: 20,  name: voice,   subnet: 10.20.20.0/24 }
  - { id: 999, name: native,  subnet: null }

uplinks:
  - { port: Ethernet49, description: "to core-1", allowed_vlans: "10,20" }
  - { port: Ethernet50, description: "to core-2", allowed_vlans: "10,20" }

The Jinja2 Template

# templates/switch_base.j2
#jinja2: lstrip_blocks: True, trim_blocks: True
hostname {{ inventory_hostname_short }}

{% for s in ntp_servers %}
ntp server {{ s }}
{% endfor %}
{% for s in syslog_servers %}
logging host {{ s }}
{% endfor %}

{% for v in vlans %}
vlan {{ v.id }}
   name {{ v.name }}
{% endfor %}

{% for u in uplinks %}
interface {{ u.port }}
   description {{ u.description }}
   switchport mode trunk
   switchport trunk allowed vlan {{ u.allowed_vlans }}
   no shutdown
{% endfor %}

Two directives earn their keep: lstrip_blocks and trim_blocks stop the loop control lines from emitting blank lines into the configuration, which matters because some platforms treat stray whitespace poorly and because a clean diff is easier to review.

Role Layout and Safe Deployment

roles/switch_base/
├── defaults/main.yml          # overridable defaults
├── vars/main.yml              # role-internal variables
├── templates/switch_base.j2
└── tasks/main.yml
- name: render and deploy base config
  hosts: switches
  gather_facts: false
  tasks:
    - name: back up the running configuration first
      ansible.netcommon.cli_command:
        command: show running-config
      register: running_before

    - name: save the backup locally
      ansible.builtin.copy:
        content: "{{ running_before.stdout }}"
        dest: "./backups/{{ inventory_hostname }}-{{ lookup('pipe','date +%Y%m%d-%H%M') }}.cfg"

    - name: render the template to a file for review
      ansible.builtin.template:
        src: switch_base.j2
        dest: "./rendered/{{ inventory_hostname }}.cfg"

    - name: apply config via the platform module
      cisco.ios.ios_config:
        src: "./rendered/{{ inventory_hostname }}.cfg"
      when: apply_changes | bool

The when: apply_changes | bool gate is how you make the playbook safe by default: the first run renders and diffs, the second (with -e apply_changes=true) applies. Platform modules such as ios_config, eos_config and junos_config also support check mode and will report the diff they would apply.

Habits That Keep This Maintainable

  • One template per platform, not per device. Device differences belong in variables; duplicated templates diverge within a quarter.
  • Keep secrets in Ansible Vault and reference them from group vars — never inline passwords in an inventory file.
  • Filter and validate data in the template (ansible.utils.ipaddr, default(), explicit failures) so a typo in a variable fails the run instead of pushing a broken VLAN.
  • Back up before and after every change, and store the rendered config with the backup so you can prove what the device should look like.
  • Pull variables from your IPAM/CMDB (NetBox, for example) rather than hand-editing YAML, and the template becomes a genuine single source of truth.

相关阅读:Ansible 网络模块:ios_config、facts 与配置备份多厂商网络自动化:Ansible 与 Jinja2 以及 NetBox IPAM:前缀、VLAN 与 IP 地址

原文链接:Ansible Network Examples - network best practices