Dynamically set variable content in Ansible playbook for ufw task - ansible

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

Related

Ansible parse_cli build object from config lines in arbitrary order

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.

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') }}

Omit os_subnet's variable dns_nameservers in Ansible

I would like to omit the dns_nameservers variable from the following Openstack function if the value does not appear in the variable file:
os_subnet:
cloud: "{{ item.cloud }}"
state: present
validate_certs: no
no_gateway_ip: yes
dns_nameservers:
- "{{ item.dns | default(None) }}"
enable_dhcp: yes
name: "{{ item.subnet }}"
network_name: "{{ item.network }}"
cidr: "{{ item.cidr }}"
allocation_pool_start: "{{ item.allocation_pool_start }}"
allocation_pool_end: "{{ item.allocation_pool_end }}"
host_routes: "{{ item.host_routes | default(omit) }}"
with_items:
- "{{ subnets }}"
tags: subnets
Until now, I have tried to omit it with | default(omit) and | default(None), but it is not working. Is any filter that might help or any other way?
EDIT:
Variable file:
- cloud: tenant1
network: nw
subnet: nw_subnet
cidr: 172.12.17.64/26
dns:
- 8.8.8.8
- 8.8.8.9
allocation_pool_start: 172.12.17.68
allocation_pool_end: 172.12.17.70
host_routes:
- destination: 0.0.0.0/0
nexthop: 172.12.17.65
I am getting the following error:
Reason: '[u'8.8.8.8', u'8.8.8.9']' is not a valid
nameserver. '[u'8.8.8.8', u'8.8.8.9']' is not a valid
IP address.\", \"type\": \"HTTPBadRequest\", \"detail\": \"\"}}"}
You want to either pass a list with a single element or pass an omit keyword (placeholder object), which tells Ansible not to pass the whole parameter (dns_nameservers here) to the module:
dns_nameservers: "{{ [item.dns] if item.dns is defined else omit }}"
In your example, if item.dns was undefined, you passed a list with a single element being an omit placeholder. In such case the dns_nameservers parameter is defined (that list which is hardcoded in the code) and behaviour is undefined (likely depends on module).

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.

Ansible loop over variables

i am using ansible to update configuration file of newly added NIC
for that i have defined some variables in separate yml file
/tmp/ip.yml
#first interface
interface1: eth1
bootproto1: static
ipaddress1: 192.168.211.249
netmask1: 255.255.255.0
gateway: 192.168.211.2
DNS1: 192.168.211.2
#second interface
interface2: eth2
bootproto2: static
ipaddress2: 10.0.0.100
netmask2: 255.0.0.0
Playbook
- include_vars: /tmp/ip.yml
- name: configuring interface
lineinfile:
state=present
create=yes
dest=/etc/sysconfig/network-scripts/ifcfg-{{interface1}}
regexp="{{ item.regexp }}"
line="{{ item.line }}"
with_items:
- { regexp: '^BOOTPROTO=.*', line: 'BOOTPROTO={{interface1}}' }
- { regexp: '^IPADDR=.*', line: 'IPADDR={{ipaddress1}' }
- { regexp: '^NETMASK=.*', line: 'NETMASK={{netmask1}}' }
- { regexp: '^GATEWAY=.*', line: 'GATEWAY={{gateway}}' }
- { regexp: '^PEERDNS=.*', line: 'PEERDNS=no' }
- { regexp: '^DNS1=.*', line: 'DNS1={{DNS1}}' }
- { regexp: '^ONBOOT=.*', line: 'ONBOOT={{onboot}}' }
when: bootproto1 == 'static'
- name: configuring for DHCP
lineinfile:
state=present
create=yes
dest=/etc/sysconfig/network-scripts/ifcfg-{{interface1}}
regexp="{{ item.regexp }}"
line="{{ item.line }}"
with_items:
- { regexp: '^BOOTPROTO=.*',line: 'BOOTPROTO={{bootproto1}}' }
- {regexp: '^PEERDNS=.*',line: 'PEERDNS=yes' }
- { regexp: '^ONBOOT=.*', line: 'ONBOOT={{onboot}}' }
when: bootproto1 == 'dhcp'
similarly repeated for second interface.
Even Though this method works for 2 NIC,this is too difficult to manage ,that is for each new NIC added i need to modify playbook and update corresponding variable in /tmp/ip.yml.
Is there a way to add variables to /tmp/ip.yml and may be using some separator parse it to playbook with out modifying playbook each time for plugging in new NIC.
There is a lot to say here.
First, try to avoid lineinfile like plague. It is really a last-resort solution. lineinfile makes it hard to write consistent and idempotents playbooks.
Now, since you're trying to populate RH style interface files, it is quite easy to do.
Organize your variables
The first thing to do is to have a proper structure for your variables. You'll want to loop over your interfaces so you have to make stuff 'loopable'. Having interface1, interface2 ... interfaceN is not scalable as you mentioned.
Here is a suggestion :
interfaces_ipv4:
- name: eth0
bootproto: static
ipaddress: 192.168.211.249
netmask: 255.255.255.0
gateway: 192.168.211.2
dns: 192.168.211.2
- name: eth2
bootproto: static
ipaddress: 10.0.0.100
netmask: 255.0.0.0
Write your template
Now that you have your data, you need a template to create your OS config file.
BOOTPROTO={{item.bootproto}}
IPADDR={{item.ipaddress}}
NETMASK={{item.netmask}}
{% if item.gateway is defined %}
GATEWAY={{item.gateway}}
{% endif %}
PEERDNS=no
DNS1={{item.dns}}
ONBOOT={{item.onboot|default('no')}}
I included two variations : you can skip outputting a line when it's not set ({% if ... %} construct) or provide default values (for instance {{item.onboot|default('no')}}).
Your mileage may vay, depending if you want to use a default or to skip with the if construct.
Create a task
Finally, here is a task that will create interface configuration files for each interface :
- name: Push template
template:
src=/path/to/the/above/template.j2
dest=/etc/sysconfig/network-scripts/ifcfg-{{item.name}}.cfg
with_items:
- "{{ interfaces_ipv4 }}"
This should do it all.
Of course, best way to use this task is to add it to some "network" role, and call it from a playbook.
Good luck.

Resources