Ansible parse_cli build object from config lines in arbitrary order - ansible

I'm trying to parse a DHCP scope from a range of Cisco IOS routers using Ansible.
< TL;DR; > I'm currently using parse_cli with a regex including multiple named matches for the same thing in different positions (e.g. domain_name_1, domain_name_2) then combining them in the output object by assigning them as domain_name: "{{ item.domain_name_1 }}{{ item.domain_name_2 }}" - as only one of them at a time will be populated, the output object still contains the correct data. I'd like to find a way to do it which doesn't require ugly inflexible hacks like that. < /TL;DR; >
I like the parse_cli Jinja filter as it provides a nice object as the output containing only the info I need, but I can't work out a way to have it cope with an arbitrary order of commands in the config.
Three examples of the same config in different order are as follows. Order of config lines can vary depending on which firmware version the router is running, the order the commands were entered in, or the model of router. The commands themselves (in this case) will always be the same so I just need a way to pull out each line and parse it appropriately.
Cisco 867
ip dhcp pool Data
import all
network 192.168.1.0 255.255.255.0
update dns
default-router 192.168.1.1
dns-server 192.168.1.1 8.8.8.8
option 42 ip 192.168.1.1
domain-name test.local
lease 8
Cisco 881
ip dhcp pool Data
import all
network 192.168.1.0 255.255.255.0
option 42 ip 192.168.1.1
default-router 192.168.1.1
dns-server 192.168.1.1 8.8.8.8
domain-name test.local
lease 8
Cisco 1111
ip dhcp pool Data
import all
network 192.168.1.0 255.255.255.0
default-router 192.168.1.1
dns-server 192.168.1.1 8.8.8.8
domain-name test.local
option 42 ip 192.168.1.1
lease 8
In all of those cases or any other new order which appears in future, I want to create an object in ansible as a fact with format:
{
"network": "192.168.1.0",
"dns-servers": ["192.168.1.1", "8.8.8.8"],
"domain-name": "test.local",
"options": {
"42": {
"type": "ip",
"value": "192.168.1.1"
}
}
}
Currently I'm doing something like this:
tasks/main.yml
- name: Get Cisco IOS DHCP pools
ios_command:
commands:
- 'show running-config | s ip dhcp pool'
retries: 1
register: ciscoios_dhcp_pool_output
- name: Convert Cisco IOS DHCP pools to list
delegate_to: 127.0.0.1
set_fact:
info_dhcp_pools: "{{ ciscoios_dhcp_pool_output.stdout[0] | parse_cli('roles/get-router-dhcp-pools/parsers/ios-pools.yml') }}"
parsers/ios-pools.yml
---
vars:
dhcp_pool:
name: "{{ item.name }}"
network: "{{ item.network_ip }}"
subnet: "{{ item.network_subnet }}"
dns_servers: "{{ item.dns_servers_1 }}{{ item.dns_servers_2 }}"
domain_name: "{{ item.domain_name_0 }}{{ item.domain_name_1 }}{{ item.domain_name_2 }}{{ item.domain_name_3 }}"
options: "{{ item.options_1 }}{{ item.options_2 }}"
lease_days: "{{ item.lease_days }}"
lease_hours: "{{ item.lease_hours }}"
lease_minutes: "{{ item.lease_minutes }}"
keys:
dhcp_pools:
value: "{{ dhcp_pool }}"
items: "^ip dhcp pool (?P<name>[^\\n]+)\\s+(?:import (?P<import_all>all)\\s*)?(?:network (?P<network_ip>[\\d.]+) (?P<network_subnet>[\\d.]+)?\\s*)?(?:update dns\\s*)?(?:host (?P<host_ip>[\\d.]+) (?P<host_subnet>[\\d.]+)\\s*)?(?:domain-name (?P<domain_name_0>[\\w._-]+)\\s+)?(?:default-router (?P<default_router>[\\d.]+)\\s*)?(?:dns-server (?P<dns_servers_1>(?:[\\d.]+ ?)+ ?)+\\s*)?(?:domain-name (?P<domain_name_1>[\\w._-]+)\\s+)?(?P<options_1>(?:option [^\\n]+\\n*\\s*)*)?(?:domain-name (?P<domain_name_2>[\\w._-]+)\\s+)?(?P<options_2>(?:option [^\\n]+\\n*\\s*)*)?(?:dns-server (?P<dns_servers_2>(?:[\\d.]+ ?)+ ?)+\\s*)?(?:domain-name (?P<domain_name_3>[\\w._-]+)\\s*)?(lease (?P<lease_days>\\d+)(?: (?P<lease_hours>\\d+))?(?: (?P<lease_minutes>\\d+))?\\s*)?(?:update arp)?"
That however feels very messy, and requires updates every time I discover a new order that config lines can appear in. It feels like the sort of thing that there should be a better way to achieve but I'm not sure on what it is.

Related

Dynamically set variable content in Ansible playbook for ufw task

I'm currently trying to find a way to dynamically create some firewall rules (with the ufw ansible module) on some hosts. In the current context, we're talking about a docker swarm.
What I want to do is to create a set of rules (different protocols, ports) per source ip, but to have that dynamically expand with the number (and IP) of host, so that whatever is the amount or IP of the nodes, it always ends up creating one set of rule PER node on each node. Ideally, I would be able to exclude the current host it's running on as well.
I'm not very familiar with ansible and I'm not sure it's possible, as it would probably require to loop within a loop when creating the rules.
I've tried to join the address within the group with ip_swarm: "{{groups['all']|join(',') }}" but ufw doesn't allow you to create rules that way and require one line per port/ip/proto it would seem (or ansible does, but looking at the code for the module, it seems to be ufw).
I'm assuming if there is a way, this is with some jinja loop in defining variables ? Or maybe with_subelements ?
Here's my current code, though that won't help as this was made for the case of a variable holding a string with comma separated values for IPs :
- name: Regles pare feu pour docker swarm
ufw:
rule: allow
direction: "{{ item.direction }}"
proto: "{{ item.proto }}"
port: "{{ item.port }}"
from_ip: "{{ item.from_ip }}"
state: reloaded
loop:
- direction: "in"
proto: "tcp"
port: "2377"
from_ip: " {{ ip_swarm }}"
- direction: "in"
proto: "tcp"
port: "7946"
from_ip: " {{ ip_swarm }}"
- direction: "in"
proto: "udp"
port: "7946"
from_ip: " {{ ip_swarm }}"
- direction: "in"
proto: "udp"
port: "4789"
from_ip: " {{ ip_swarm }}"
Here you go :) If you have some kind of idea, I'll gladly take it. Have a good day !
Well, turns out it was much simpler than what I intended. Ansible does have nested loop, with either with_nested, or by using non depreceated options, loop with the product filter (the first option being easier to read). See https://docs.ansible.com/ansible/latest/user_guide/playbooks_loops.html
My vars file that contains the FW parameters (for indication) :
---
liste_regle_pare_feu:
- direction: "in"
proto: "tcp"
port: "2377"
- direction: "in"
proto: "tcp"
port: "7946"
- direction: "in"
proto: "udp"
port: "7946"
- direction: "in"
proto: "udp"
port: "4789"
and the task itself :
- name: Regles pare feu pour docker swarm
ufw:
rule: allow
direction: "{{ item.1.direction }}"
proto: "{{ item.1.proto }}"
port: "{{ item.1.port }}"
from_ip: "{{ item.0 }}"
loop: "{{ groups['all']|product(liste_regle_pare_feu)|list }}"
the loop will, for each item in the first object (here groups['all'], the list of all IPs), iterate over my rule list (of hashtables). See documentation for details on product filter

Gather a List of Configured IP Addresses

I want to pull out the lines of a Juniper config that assign an IPv6 address to an interface and save that output to a file.
The output I am after is generated with the command 'show configuration | display set| match "inet6 address" '
I'm building an Ansible playbook and have pinballed off of errors to end up with the below task. It is basically giving me the complete interface configs, and I just want to narrow it down to the lines that would fit the "match" line in the manual command. The commented out filters aren't working and I can't find documentation that explains filters in a way that I understand.
- name: "Get selected configuration hierarchies"
juniper_junos_config:
host: "{{ ansible_host }}"
retrieve: "committed"
config_mode : "private"
filter: "<configuration><interfaces/></configuration>"
#filter: "<configuration><interfaces/><family/><inet6/><address/></configuration>"
#filter: "inet6/address"
format: "set"
options:
inherit: "inherit"
dest: "{{ inventory_hostname }}-inet6-config"
register: response
- name: "Print result"
debug:
var: response
Output:
ok: [LAB-QFX5110-1] => {
"response": {
"changed": false,
"config": "\nset interfaces xe-0/0/0 apply-groups-except jumbo-frames\nset interfaces xe-0/0/0 description \"Test Laptop - DMZ;000\"\nset interfaces xe-0/0/0 gigether-options 802.3ad ae12\n<SNIP>\nset interfaces lo0 unit 10 family inet address 100.126.0.x/32\nset interfaces lo0 unit 10 family inet6 address ABCD:EF00:0000:01c4::1/128\n<SNIP>/n",
"config_lines": [
"",
"set interfaces xe-0/0/0 apply-groups-except jumbo-frames",
"set interfaces xe-0/0/0 description \"Test Laptop - DMZ;000\"",
"set interfaces xe-0/0/0 gigether-options 802.3ad ae12",
"<SNIP>",
"set interfaces lo0 unit 10 family inet address 100.126.0.x/32",
"set interfaces lo0 unit 10 family inet6 address ABCD:EF00:0000:01c4::1/128",
"<SNIP>",
],
"failed": false,
"msg": "Configuration has been: opened, retrieved, closed."
}
}
I just want the lines that read:
set interfaces unit X family inet6 address XXXX:YYYY:ZZZZ:1234::1/127
But I can't seem to plug in the correct filter.
I will also mention that if there is a better way to gather this, I am open to exploring it. It just seems like this is the task Ansible was created to perform.
here is how to do it. since your response dictionary contains the output split by lines, we will use the config_lines key, process line by line:
code:
---
- hosts: localhost
gather_facts: false
vars:
response:
changed: false
config: |2-
set interfaces xe-0/0/0 apply-groups-except jumbo-frames
set interfaces xe-0/0/0 description "Test Laptop - DMZ;000"
set interfaces xe-0/0/0 gigether-options 802.3ad ae12
<SNIP>
set interfaces lo0 unit 10 family inet address 100.126.0.x/32
set interfaces lo0 unit 10 family inet6 address ABCD:EF00:0000:01c4::1/128
<SNIP>/n
config_lines:
- ''
- set interfaces xe-0/0/0 apply-groups-except jumbo-frames
- set interfaces xe-0/0/0 description "Test Laptop - DMZ;000"
- set interfaces xe-0/0/0 gigether-options 802.3ad ae12
- "<SNIP>"
- set interfaces lo0 unit 10 family inet address 100.126.0.x/32
- set interfaces lo0 unit 10 family inet6 address ABCD:EF00:0000:01c4::1/128
- "<SNIP>"
failed: false
msg: 'Configuration has been: opened, retrieved, closed.'
tasks:
- name: find entries containing inet6 address, add to results
set_fact:
interfaces: "{{ interfaces | default([]) + [item] }}"
when: item is search('inet6 address')
with_items:
- "{{ response.config_lines }}"
- debug:
var: interfaces
- name: save results to file
template:
src: results.j2
dest: /tmp/results.out
you will need a jinja2 template for the last task to work (under same dir as your playbook), with contents:
results.j2:
# processing results:
{% for interface in interfaces -%}
{{ interface }}
{%- endfor %}
1st task parses each line and if the when condition is met, its adding the given line to a results , the interfaces list. 2nd task prints that variable. 3rd task saves the results to a file under /tmp/.
hope it helps

Ansible to get the netmask in CIDR

In order to get the value "24" corresponding to an IP address/networkmask, I have this working piece of code:
- set_fact:
ip: "{{ ansible_default_ipv4.address }}/{{ansible_default_ipv4.netmask }}"
- set_fact:
mask_cidr: "{{ ip | ipaddr('prefix') }}"
Where ansible_default_ipv4.address = 172.16.1.67 and ansible_default_ipv4.netmask is 255.255.255.0 as per gather_facts or setup module.
I've tried different things to make this code "smarter" so I only need to set 1 fact instead of 2, but my filtering abilities are not strong.
Any ideas in how to convert these two facts in a smarter fact that do both things?
Easiest and the most clean way is through a helper variable:
- set_fact:
mask_cidr: "{{ ip | ipaddr('prefix') }}"
vars:
ip: "{{ ansible_default_ipv4.address }}/{{ansible_default_ipv4.netmask }}"
If you insist on writing a single template:
- sef_fact:
ip: "{{ (ansible_default_ipv4.address + '/' + ansible_default_ipv4.netmask) | ipaddr('prefix') }}"
I'm using this:
mynetwork = {{ (ansible_default_ipv4.network + '/' + ansible_default_ipv4.netmask) | ipaddr('network/prefix') }}

Ansible create var from hosts

I am using ansible to create a env var list of hosts and ports to be used for a database connection string.
The application requires two vars: a host list and a port list and uses that to create a connection string.
hosts: 127.0.0.1,127.0.0.1,127.0.0.1
ports: 123,123,123
host and port are matched based on index postion.
I am able to get hosts and join them as required. What I am unable to do is dynamically create a ports string based on the number of hosts. We are currently using the same port so it should be easier.
What I would like to do is create ports:123,123,123 where the number of times 123 is repeated is equal to the number of hosts.
Looked at this link for gettingnumber of hsots: Ansible: Get number of hosts in group
How i just need to print 123 that numebr of times and assign it to ports.
You could use sequence plugin
vars:
hosts: ['127.0.0.1','127.0.0.1','127.0.0.1']
- name: ports fact with size of hosts
set_fact:
ports: "{{ ports | default([]) + [123]}}"
with_sequence: count="{{ hosts | length }}"
- debug: msg="{{ ports }}"
TASK [debug] ********************* ok: [localhost] => {
"ports": [
123,
123,
123
] }
Source: https://docs.ansible.com/ansible/2.5/plugins/lookup/sequence.html

Search Dictionary Values in Ansible

Having a dictionary like this:
ossec_datacenter:
atlanta:
hostname: 'server1.fakedomain.net'
ip: '192.168.12.170'
port: '1515'
miami:
hostname: 'server2.fakedomain.net'
ip: '192.168.20.31'
port: '1514'
dallas:
hostname: 'server2.fakedomain.net'
ip: '192.168.20.20'
port: '1515'
How would I search for all values in this dictionary in my when clause?
I can access variables using ossec_datacenter[ossec_dc]['hostname']
But I want so search all values to make sure no matches are present.
In other words I don't want the inventory_hostname nor the IP to be found anywhere in that data structure.
If you want to use json_query (requires ansible 2.2) you can do this to search ip and hostname:
- name: find inventory_hostname
set_fact:
found: True
with_items:
- "{{ ossec_datacenter | json_query('*.ip') }}"
- "{{ ossec_datacenter | json_query('*.hostname') }}"
when: "inventory_hostname == item"
or if you want to search any of the keys in the datacenters (ip, hostname, or port):
- name: find inventory_hostname
set_fact:
found: True
with_items: "{{ ossec_datacenter | json_query('*.*') }}"
when: "inventory_hostname == item"
and then test the found var.
Here's a condition for hostname:
when: inventory_hostname not in (ossec_datacenter.values() | map(attribute='hostname') | list)
Use ansible_default_ipv4.address or some other fact about IP address and reduce your dict with map(attribute='ip') to search for IP addresses.

Resources