Generate single configuration for multiple hosts from Jinja2 template - ansible

Problem: I've got 2 tasks that are executed sequentially as (expected) given the Ansible nature. As I understand, you can only execute one module per each task.
Task 1 - Gather facts information (serials, versions, etc) from network devices.
Task 2 - Render a template with the info gathered in Task1
Ideal Result: Since I'm looping through loads of network devices my ideal result would be to choose one device at a time, gather the information from it and then render a template with that information, next go to the other device on the loop and so on.
Approach: I've thought to keep the same syntax, On task 1 save the facts on a file (.json) and on task 2, read the JSON file and get the variables I'm interested in.
Is there any better way to do this? (Probably there's more than one)
What I've got currently doesn't fit my purpose as when the template renders, it only contains information about the last device:
Tasks: roles/juniper.junos/tasks/main.yaml
- name: 1 - Gathering Facts
junos_get_facts:
host: "{{ inventory_hostname}}"
user: ""
passwd: ""
savedir: "~/Ansible/Ouput/Facts"
ignore_errors: True
register: junos
- name: 2 - Creating the template
template:
src="~/Ansible/roles/juniper.junos/templates/template.j2"
dest="~/Ansible/Ouput/Facts/Device_facts.yml"
Template: ~/Ansible/roles/juniper.junos/templates/template.j2
{% for host in groups['OOB_AMS'] %}
ANSIBLE NAME: {{ inventory_hostname}}
HOSTNAME: {{ junos.facts.hostname }}
MODEL: {{ junos.facts.model }}
SERIAL: {{ junos.facts.serialnumber }}
VERSION: {{ junos.facts.model }}
UP TIME: {{ junos.facts.RE0.up_time }}
{% endfor %}
IDEAL Output:"~/Ansible/Ouput/Facts/Device_facts.yml"
ANSIBLE NAME: DEVICE 1
HOSTNAME: DEVICE 1 HOSTNAME
MODEL: DEVICE 1 MODEL
SERIAL: DEVICE 1 SERIAL
VERSION: DEVICE 1 VERSION
UP TIME: DEVICE 1 UP TIME
ANSIBLE NAME: DEVICE 2
HOSTNAME: DEVICE 2 HOSTNAME
MODEL: DEVICE 2 MODEL
SERIAL: DEVICE 2 SERIAL
VERSION: DEVICE 2 VERSION
UP TIME: DEVICE 2 UP TIME
ANSIBLE NAME: DEVICE 3
HOSTNAME: DEVICE 3 HOSTNAME
MODEL: DEVICE 3 MODEL
SERIAL: DEVICE 3 SERIAL
VERSION: DEVICE 3 VERSION
UP TIME: DEVICE 3 UP TIME

You wrote a for-loop with a host variable, but not used it even once.
Change the template to:
{% for host in ansible_play_hosts %}
ANSIBLE NAME: {{ hostvars[host].inventory_hostname}}
HOSTNAME: {{ hostvars[host].junos.facts.hostname }}
MODEL: {{ hostvars[host].junos.facts.model }}
SERIAL: {{ hostvars[host].junos.facts.serialnumber }}
VERSION: {{ hostvars[host].junos.facts.model }}
UP TIME: {{ hostvars[host].junos.facts.RE0.up_time }}
{% endfor %}
Looping over groups['OOB_AMS'] was not bad, but hardcoding group name seems unnecessary for your case. Instead you can use: ansible_play_hosts (play_hosts before version 2.2).
Also for clarity you might add run_once: true to the template task. This is not critical, because the template generates the same output in each iteration, so the subsequent runs will be skipped anyway, but there is no need to do it several times.

Related

Ansible Update multiple firewall rules on Vyos

I've been trying to create a playbook that I can run periodically to go through all my Vyos firewall rules and ensure the "log enabled" command is present, just in case someone forgets to add logging to a firewall rule. I've found the vyos.vyos.vyos_firewall_rules module which I think will be perfect for what I need to do.
The only problem is, is that this module requires you input the rule set name and rule number of each firewall rule that you want to update. However in my case, I want this to be done automatically and Ansible to go through each firewall rule set and associated rules that are present, and ensure logging is enabled on each rule. Something like this is what I need:
- name: Enable logging for each firewall rule
vyos.vyos.vyos_firewall_rules:
config:
- afi: ipv4
rule_sets:
- name: *all rules sets*
rules:
- number: *all numbers*
log: enabled
I've used vyos.vyos.vyos_firewall_rules to gather a dump of all rule sets and associated rules and have filtered this down to list each rule set name along with each associated rule number :
- name: Get rulesets
vyos.vyos.vyos_firewall_rules:
config:
state: gathered
register: output
- name: Filter output and populate the list of rule set names
debug:
msg: "Rule set name: {{ item.0.name }}, rule number: {{ item.1.number }}"
loop: "{{ output.gathered[0]['rule_sets'] | subelements('rules') }}"
This produces output like this:
"Rule set name: ruleset-1, rule number: 1"
"Rule set name: ruleset-1, rule number: 2"
"Rule set name: ruleset-1, rule number: 15"
"Rule set name: ruleset-1, rule number: 20"
"Rule set name: ruleset-2, rule number: 1"
"Rule set name: ruleset-2, rule number: 2"
I'm a bit stuck on where to go from here. I feel like I need the info filtered into a nested list like I have below, and then somehow loop the vyos.vyos.vyos_firewall_rules module to update each rule set name and rule number.
firewall_rules:
ruleset-1:
1
2
15
20
ruleset-2
1
2
I haven't been able to figure out how to create a nested list, or if I even need one in the first place.
I'm relatively new to Ansible so if anyone could point me in the right direction I would appreciate it.
You can create the dictionary first
- set_fact:
firewall_rules: "{{ dict(rsets|zip(rules)) }}"
vars:
rsets: "{{ output.gathered.0.rule_sets|
map(attribute='name')|list }}"
rules: "{{ output.gathered.0.rule_sets|
map(attribute='rules')|
map('map', attribute='number')|list }}"
gives
firewall_rules:
ruleset-1:
- 1
- 2
- 15
- 20
ruleset-2:
- 1
- 2
Then use Jinja to create the structure, e.g.
- debug:
var: _config|from_yaml
vars:
_config: |-
- afi: ipv4
rule_sets:
{% for set, rules in firewall_rules.items() %}
- name: {{ set }}
rules:
{% for rule in rules %}
- number: {{ rule }}
{% endfor %}
{% endfor %}
gives
_config|from_yaml:
- afi: ipv4
rule_sets:
- name: ruleset-1
rules:
- number: 1
- number: 2
- number: 15
- number: 20
- name: ruleset-2
rules:
- number: 1
- number: 2
Fit the structure to your needs and use it in the module, e.g.
- vyos.vyos.vyos_firewall_rules:
config: "{{ _config|from_yaml }}"
vars:
_config: |-
...
Q: "Output of {{ _config|from_yaml }} is not preserving the hyphens."
A: The format you see depends on the callback plugin. If you want to see YAML set DEFAULT_STDOUT_CALLBACK to yaml
shell> ANSIBLE_STDOUT_CALLBACK=yaml ansible-playbook test-753.yml
or copy the content to a file
- copy:
dest: test-753-out.yml
content: |
{{ _config }}
vars:
_config: |-
- afi: ipv4
rule_sets:
{% for set, rules in firewall_rules.items() %}
- name: {{ set }}
rules:
{% for rule in rules %}
- number: {{ rule }}
{% endfor %}
{% endfor %}
gives
shell> cat test-753-out.yml
- afi: ipv4
rule_sets:
- name: ruleset-1
rules:
- number: 1
- number: 2
- number: 15
- number: 20
- name: ruleset-2
rules:
- number: 1
- number: 2

Missing variable in jinja2, no output with Ansible

I have an Ansible playbook that gathers facts from Cisco switches.
---
- hosts: switches
gather_facts: False
connection: network_cli
vars:
backup_root: ./configs
cli:
host: "{{ inventory_hostname }}"
tasks:
- name: ensure device folder is created
file:
path: "{{ backup_root }}/{{ inventory_hostname }}"
state: directory
- name: Gather all facts
cisco.ios.ios_facts:
gather_subset: all
- name: Serial Number
debug: var=ansible_net_serialnum
- name: Model
debug: var=ansible_net_model
- name: Hostname
debug: var=ansible_net_hostname
- name: Version
debug: var=ansible_net_version
- name: CDP
debug: var=ansible_net_neighbors
- name: Config file
debug: var=ansible_net_config
- name: Stack SW Model Numbs
debug: var=ansible_net_stacked_models
- name: Stack SW Model Numbs
debug: var=ansible_net_stacked_serialnums
- name: Get VLAN Info
cisco.ios.ios_command:
commands: show vlan brief
register: show_vlan
- name: get timestamp
command: date +%Y%m%d
register: timestamp
- name: Generate configuration files
template:
src=roles/discovery/templates/ios_switches.j2
dest="{{ backup_root }}/{{ inventory_hostname }}/{{ inventory_hostname }}.txt" `
Here is the jinja file.
Hostname: {{ ansible_net_hostname }}
Model: {{ansible_net_model}}
Serial Number: {{ansible_net_serialnum}}
IOS Version: {{ansible_net_version}}
IOS Image: {{ansible_net_image}}
Switch Stack Models:
{{ansible_net_stacked_models | to_nice_yaml(indent=2)}}
Switch Stack Serials:
{{ansible_net_stacked_serialnums | to_nice_yaml(indent=2)}}
CDP Neighbors:
{{ansible_net_neighbors | to_nice_yaml(indent=2)}}
Configuration:
{{ansible_net_config}}
VLAN:
{{show_vlan.stdout[0] | to_nice_yaml(indent=2)}}
This all works fine until it hits a switch that cannot stack (e.g. chassis or VSS). When I run the playbook, I get the following-
msg: 'AnsibleUndefinedVariable: ''ansible_net_stacked_models'' is undefined
I've tried using if in Jinja2 like the following
...
Switch Stack Models:
{% if ansible_net_stacked_models is not defined %}
NOT A STACKABLE SWITCH
{% else %}
{{ansible_net_stacked_models | to_nice_yaml(indent=2)}}
{% endif %}
however it fails in the Jinja rendering and does not produce any output.
Is there a way to ignore missing variables in jinja or is there a better way to do this?
you can set a default value to your variable if is not defined
{{ ansible_net_stacked_models|default("NOT A STACKABLE SWITCH", true) | to_nice_yaml(indent=2) }}
Come to find out the error was stopping Ansible from even calling the jinja file. I dug around some more and found that I could set the variable to a default value in the inventory.
[switch:vars]
ansible_net_stacked_models='NOT A STACKABLE SWITCH'
When running the playbook if the device has valid info it will overwrite the default variable we defined in inventory. If the device doesn't have valid info, Ansible will simply pass the default variable we set in inventory, over to jinja2.

Ansible Jinja Template config and stdout comparison

My pseudocode:
1. Get the ntp server config from "sh run"
2. Store that to a list
3. Jinja template generates the required config. I am passing the ntp_server IPs via -e (extra variables).
4. Add the config from 3, compare 3 and 4 and remove the rest.
I am struggling on step 4 [comparison part]. How do I compare the current config with the config generated from the jinja template? I am using roles.
Please advise.
# Jinja Template
{% for ntp_srv in ntp_servers %}
ntp server {{ ntp_srv }}
{% endfor %}
# tasks file for ansible-ios-ntp
---
- name: Current Edge servers before
ios_command:
commands:
- sh run | include ntp server
register: runconfser
- debug:
var: runconfser
# NTP SECTION - START
- name: Set NTP servers
ios_config:
src: ntprequired.j2
notify: Save Config
- name: Remove the rest NTP Servers
with_items: "{{ runconfser.stdout_lines[0] }}"
when: (item not in {src: 'ntprequired.j2'} and (item!=""))
ios_config:
lines:
- "no {{ item }}"
If I understand your question correctly, I believe you'd want to extract the current IPs from the registered output, then capture the ones which are not in the ntp_servers list:
- set_fact:
need_ips: |
{{ ntp_servers | difference(stdout_lines | join(" ") | regex_findall('[0-9.]+')) }}
Or you can obtain the "extra" ones by inverting the order of the difference:
- set_fact:
extra_ips: |
{{ stdout_lines | join(" ") | regex_findall('[0-9.]+') | difference(ntp_servers) }}
I cheated by just searching for [0-9.]+ but you can, of course, make that expression less tolerant by being more specific (aka [1-9](.[0-9.]){3})

loop using with_sequence over with_subelement in ansible

I am trying to loop over a subelement variables using the a loop like with_sequence,
For the moment I have :
---
- hosts: corosync
gather_facts: no
vars:
host_list:
- node_one
- node_two
list_services:
- group: ALPHA
services:
- name: DHCP
directory: /etc/dhcp
- name: DNS
directory : /etc/dns
- group: BETA
services:
- name: SSH
directory: /etc/ssh
- name: FTP
directory: /ztc/ftp
tasks:
- name: create group-services
debug:
msg: "the service name is {{ item.0.group}}-{{ item.1.name}} , directory is {{ item.1.directory }}"
with_subelements:
- "{{ list_services }}"
- services
Since I have 2 nodes in my cluster
node_one
node_two
I want to deplucate each service like below :
{{ item.0.group}}-{{host_id}}-{{ item.1.name}}
with {{ host_id }} a list that equal ['0','1'] since I have 2 nodes
and the with_subelement function loop over the {{ host_id }} twice since we have two nodes, what gives :
ALPHA-0-DHCP
ALPHA-0-DNS
ALPHA-1-DHCP
ALPHA-1-DNS
BETA-0-SSH
BETA-0-FTP
BETA-1-SSH
BETA-1-FTP
I want to use something like with_sequence function beside with_subelement like
with_sequence: start=0, end={{ groups['host_list']|length}}
Any suggestions please
The loop declaration introduced in Ansible 2.5 makes it pretty straightforward ー you just need to combine the two patterns replacing legacy with_sequence and legacy with_subelements:
- name: create group-services
debug:
msg: "{{item.1.0.group}}-{{item.0}}-{{item.1.1.name}}"
loop: "{{ range(0, host_list|length) | product(list_services|subelements('services')) | list }}"

ansible how to use a variable in a for loop

I have an ansible task like this:
- name: coreos network configuration
{% for interface in argument['interfaces'] %}
{% if argument[interface]['role'] == 'ingest' %}
script: netconfiginput.sh -i {{interface}} #incorrect, how to get the value of the interface variable of the for loop?
{% endif %}
{% endfor %}
While running this ansible task, I pass a JSON string argument:
ansible-playbook --extra-vars 'argument={"interfaces":["eno1","eno2","ep8s0"],"eno2":{"role":"ingest"}}' network-config.yml
What I want to do is, loop through the JSON array called interfaces, which are a list of network interfaces, when the role of the interface is called ingest, I run a script and pass the network interface as an argument to the script, my implementation is incorrect, how can I do this?
You need to use with_items and replace variable name with item.
A rough example:
name: task name
script: netconfiginput.sh -i {{ item }}
with_items: interfaces_array
when: some_role == 'ingest'
To understand what kind of data you're sending, use the following:
name: debugging
debug:
var: argument
That should show you, amongst other things, whether or not Ansible is considering parts of your variable's structure valid arrays or not.
Jinja2 can be used in ansible templates, not in playbooks.
Ansible supports looping over hashes. You can try this:
---
- hosts: <test_servers> # replace with your hosts
vars:
interfaces:
eno1:
role: null
eno2:
role: ingest
ep8s0:
role: null
tasks:
- name: coreos network configuration
script: netconfiginput.sh -i {{ item.key }}
with_dict: "{{interfaces}}"
when: item.value.role == "ingest"

Resources