Compare commits

..

10 commits

Author SHA1 Message Date
Timotej Lazar 668af8bdb6 firewall: use a handler to reboot 2024-05-19 10:10:02 +02:00
Timotej Lazar 0e9dac6985 fabric: support arbitrary port breakouts
Not that we use anything but 1x and 4x. Mainly done so I can drop
nonexistent (because they have been broken out) interfaces from NetBox.
2024-05-13 17:44:39 +02:00
Timotej Lazar 16f34c4502 Don’t gather facts when setting them 2024-05-13 17:39:47 +02:00
Timotej Lazar 8c82af23e4 firewall: also configure VPN forwards in the app
There we can define forwards only for networks with actual VPN users.
2024-05-03 11:27:27 +02:00
Timotej Lazar 7656c05b2d Revert "firewall: configure NAT from NetBox data"
Changed my mind. All NAT and VPN is configured from the app now.
2024-04-30 20:59:49 +02:00
Timotej Lazar 8a9d47f176 firewall: configure NAT from NetBox data
This is dynamic NAT for (mostly) physical networks. NAT for custom
prefixes can still be defined in the app.
2024-04-28 15:54:01 +02:00
Timotej Lazar 457ab7d3b7 Query prefixes once for all hosts
And group them into vrf_prefixes for VLAN networks and bgp_prefixes for
servers plugged directly into fabric.

This should reduce the number of queries to NetBox when configuring
firewalls and exit switches. Not sure but I think set_fact helps to
avoid queries (as opposed to setting group_vars).
2024-04-28 12:14:05 +02:00
Timotej Lazar 1c0709a6a6 fabric: allow all VLANs on bridge
Don’t try to guess what should be allowed because not all switch links
are tagged in NetBox. For now we limit mainly on access switches.
2024-04-27 11:30:20 +02:00
Timotej Lazar c07c03a430 Set default inventory 2024-04-27 11:04:02 +02:00
Timotej Lazar 2443a90bc5 fabric: use FHRP groups for virtual router IPs
More realistic- and supported-like and also avoids duplicated gateway
addresses.
2024-04-14 15:15:48 +02:00
17 changed files with 75 additions and 88 deletions

View file

@ -24,11 +24,11 @@ Create a read-only token in NetBox. Set variables required to access NetBox:
Run one-off tasks with (add `--key-file` or other options as necessary):
ansible -i inventory.yml -m ping 'spine-*'
ansible -m ping 'spine-*'
Run a playbook with:
ansible-playbook setup.yml -i inventory.yml -l 'spine-*'
ansible-playbook setup.yml -l 'spine-*'
## NetBox data

View file

@ -1,5 +1,6 @@
[defaults]
nocows = true
filter_plugins = filter_plugins
inventory = inventory.yml
remote_user = root
vault_identity = network
filter_plugins = filter_plugins

View file

@ -1 +0,0 @@
vlans: "{{ query('netbox.netbox.nb_lookup', 'vlans', api_filter='group=new-net', raw_data=true) | sort(attribute='vid') }}"

View file

@ -213,17 +213,15 @@ ipv6 prefix-list default permit ::/0
ip prefix-list fabric permit 10.34.0.0/24 ge 32
ipv6 prefix-list fabric permit 2001:1470:fffd:3400::/64 ge 128
{% for vrf in inside_vrfs %}
{% set prefixes = query('netbox.netbox.nb_lookup', 'prefixes', api_filter='vrf_id='~vrf.id, raw_data=true)
| sort(attribute='family.value') %}
{% for prefix in prefixes %}
{% for prefix in vrf_prefixes
| selectattr('vrf.id', 'in', inside_vrfs|map(attribute='id'))
| sort(attribute='family.value') | sort(attribute='vlan.vid') %}
{% if prefix.family.value == 4 %}
ip prefix-list office permit {{ prefix.prefix }} ge 24
{% else %}
ipv6 prefix-list office permit {{ prefix.prefix }} ge 64
{% endif %}
{% endfor %}
{% endfor %}
{% if wg_net is defined %}
ip prefix-list vpn permit {{ wg_net | ipaddr('subnet') }}
@ -237,7 +235,7 @@ ip prefix-list nat permit {{ wg_ip | ipaddr('host') }}
ip prefix-list nat permit {{ network }}
{% endfor %}
{% for prefix in query('netbox.netbox.nb_lookup', 'prefixes', raw_data=true, api_filter='role=bgp') | selectattr('tenant') %}
{% for prefix in bgp_prefixes | sort(attribute='family.value') %}
{% if prefix.family.value == 4 %}
ip prefix-list dc permit {{ prefix.prefix }} ge 32
{% else %}
@ -281,7 +279,7 @@ route-map inside-import permit 21
match ipv6 address prefix-list office
# Route maps for advertised and received routes.
# Inside ↔ fabric.
# Default VRF ↔ fabric.
route-map default->fabric permit 10
match ip address prefix-list default
route-map default->fabric permit 11
@ -296,7 +294,7 @@ route-map fabric->default permit 20
route-map fabric->default permit 21
match ipv6 address prefix-list dc
# Inside ↔ firewall.
# Inside VRF ↔ firewall.
route-map inside->firewall permit 1
match interface lo
route-map inside->firewall permit 20
@ -313,7 +311,7 @@ route-map firewall->inside permit 10
route-map firewall->inside permit 11
match ipv6 address prefix-list default
# Outside ↔ firewall.
# Outside VRF ↔ firewall.
route-map outside->firewall permit 10
match ip address prefix-list default
route-map outside->firewall permit 11

View file

@ -1,5 +1,5 @@
{% set dhcp_networks = query('netbox.netbox.nb_lookup', 'prefixes', api_filter='role=dhcp-pool', raw_data=true)
| selectattr('vlan') | map(attribute='vlan.vid') | sort -%}
{% set dhcp_vlans = vrf_prefixes | selectattr('custom_fields.dhcp_ranges')
| map(attribute='vlan.vid') | sort -%}
# What servers should the DHCP relay forward requests to?
SERVERS="{{ dhcp }}"
@ -10,7 +10,7 @@ SERVERS="{{ dhcp }}"
# This will be used in the actual dhcrelay command
# For example, "-i eth0 -i eth1"
INTF_CMD="{{ interfaces | selectattr('parent') | selectattr('parent.name', '==', 'bridge')
| selectattr('untagged_vlan') | selectattr('untagged_vlan.vid', 'in', dhcp_networks)
| selectattr('untagged_vlan') | selectattr('untagged_vlan.vid', 'in', dhcp_vlans)
| map(attribute='name') | sort | map('regex_replace', '^', '-id ') | join(' ') }} -iu {{ iface_uplink }} -iu peerlink.4"
# Additional options that are passed to the DHCP relay daemon?

View file

@ -3,7 +3,7 @@
{# interfaces that belong to this bridge #}
{% set ports = interfaces | selectattr('enabled') | selectattr('bridge') | selectattr('bridge.name', '==', bridge.name) %}
{# allowed VLANs can be specified on the bridge, any of its ports, or all VLANs in NetBox #}
{% set my_vlans = bridge.tagged_vlans or (ports | iface_vlans | flatten | sort(attribute='vid') | unique) or vlans %}
{% set my_vlans = bridge.tagged_vlans or vlans %}
{% set my_vlan_ids = my_vlans | map(attribute='vid') | sort -%}
auto {{ bridge.name }}

View file

@ -1,11 +1,7 @@
# https://docs.nvidia.com/networking-ethernet-software/cumulus-linux/Layer-1-and-Switch-Ports/Interface-Configuration-and-Management/Switch-Port-Attributes/#breakout-ports
{% for interface in interfaces | selectattr('name', 'match', '^swp[0-9]+$') %}
{{ interface.name|regex_replace('^swp', '') }}=
{%- if interfaces|selectattr('name', 'match', '^'+interface.name+'s[0-9]+$') %}
4x
{% elif not interface.enabled %}
disabled
{% else %}
1x
{% endif %}
{% for interface in interfaces | selectattr('name', 'match', '^swp[0-9]+(s0)?$') %}
{# get '1' from 'swp1' and '2' from 'swp2s0' #}
{% set port = interface.name | regex_replace('^swp([0-9]+).*$', '\\1') %}
{% set count = interfaces | selectattr('name', 'match', '^swp'+port+'(s[0-9])*$') | length %}
{{ port }}={% if interface.enabled or count > 1 %}{{ count }}x{% else %}disabled{% endif +%}
{% endfor %}

View file

@ -1,3 +1,6 @@
{% set fhrp_assignments = query('netbox.netbox.nb_lookup', 'fhrp-group-assignments', raw_data=true) %}
{% set fhrp_groups = query('netbox.netbox.nb_lookup', 'fhrp-groups', raw_data=true) -%}
{% for iface in interfaces | rejectattr('name', 'in', ('lo', 'bridge')) | rejectattr('mgmt_only') | selectattr('enabled') %}
auto {{ iface.name }}
iface {{ iface.name }}
@ -37,13 +40,13 @@ iface {{ iface.name }}
{% endif %}
{#- Addresses. #}
{% for addr in iface.ip_addresses | rejectattr('role') %}
{% for addr in iface.ip_addresses %}
address {{ addr.address }}
{% endfor %}
{% set anycast = iface.ip_addresses | selectattr('role') | selectattr('role.value', '==', 'anycast')
| map(attribute='address') %}
{% if anycast %}
address-virtual 00:00:5e:00:01:01 {{ anycast | ipaddr(1) | join(' ') }}
{% if iface.count_fhrp_groups > 0 %}
{% set fhrp_assignment = fhrp_assignments | selectattr('interface.id', '==', iface.id) | first %}
{% set fhrp_group = fhrp_groups | selectattr('id', '==', fhrp_assignment.group.id) | first %}
address-virtual 00:00:5e:00:01:01 {{ fhrp_group.ip_addresses | sort(attribute='family') | map(attribute='address') | join(' ') }}
{% endif %}
{% endfor %}

View file

@ -0,0 +1,16 @@
# Make expensive lookups to NetBox once for later reference by any host.
- name: Lookup networks and prefixes
set_fact:
vlans: '{{ query("netbox.netbox.nb_lookup", "vlans", api_filter="group=new-net", raw_data=true)
| sort(attribute="vid") }}'
prefixes: '{{ query("netbox.netbox.nb_lookup", "prefixes", raw_data=true)
| sort(attribute="prefix") | sort(attribute="family.value") }}'
- name: Select VLAN and BGP prefixes
set_fact:
vrf_prefixes: '{{ prefixes | selectattr("vrf")
| selectattr("vlan") | selectattr("vlan.id", "in", vlans|map(attribute="id"))
| sort(attribute="vlan.vid") }}'
bgp_prefixes: '{{ prefixes | selectattr("tenant")
| selectattr("role") | selectattr("role.slug", "==", "bgp")
| sort(attribute="tenant.slug") }}'

View file

@ -6,6 +6,10 @@
command: mkinitfs
when: "'handler' not in ansible_skip_tags"
- name: reboot
reboot:
when: "'handler' not in ansible_skip_tags"
- name: reload frr
command: /usr/lib/frr/frr-reload.py --reload /etc/frr/frr.conf
when: "'handler' not in ansible_skip_tags"

View file

@ -3,23 +3,13 @@
dest: /etc/network/interfaces.d/mgmt.intf
src: mgmt.intf.j2
mode: 0644
register: task_mgmt_interface
notify: reboot
- name: Run SSH in management VRF
lineinfile:
path: /etc/conf.d/sshd
regexp: "#* *vrf="
line: "vrf=\"mgmt\""
register: task_ssh_vrf
notify: reboot
- name: Reboot for new VRF
reboot:
when: task_mgmt_interface.changed or task_ssh_vrf.changed
register: task_reboot
- name: Reset the connection
meta: reset_connection
- name: Wait for the network device to reload
wait_for_connection:
delay: 10
when: task_reboot.changed
- meta: flush_handlers

View file

@ -82,16 +82,13 @@ ipv6 prefix-list default permit ::/0
ip prefix-list fabric permit 10.34.0.0/24 ge 32
{% for vlan in vlans %}
{% for prefix in query('netbox.netbox.nb_lookup', 'prefixes', api_filter='vlan_id='~vlan.id, raw_data=true) %}
{% if prefix.vrf and prefix.vrf.name != 'outside' %}
{% for prefix in vrf_prefixes | rejectattr('vrf.name', '==', 'outside')
| sort(attribute='family.value') %}
{% if prefix.family.value == 4 %}
ip prefix-list office permit {{ prefix.prefix }} ge 24
{% elif prefix.family.value == 6 %}
ipv6 prefix-list office permit {{ prefix.prefix }} ge 64
{% endif %}
{% endif %}
{% endfor %}
{% endfor %}
{% if wg_net is defined %}

View file

@ -1,18 +1,9 @@
{% for vlan in vlans %}
{% set prefixes = query('netbox.netbox.nb_lookup', 'prefixes', api_filter='vlan_id='~vlan.id, raw_data=true) %}
{% set prefixes4 = prefixes | selectattr('family.value', '==', 4) | map(attribute='prefix') %}
{% set prefixes6 = prefixes | selectattr('family.value', '==', 6) | map(attribute='prefix') %}
set {{ vlan.name }} {
type ipv4_addr; flags interval
{% if prefixes4 %}
elements = { {{ prefixes4 | join(', ') }} }
{% endif %}
}
set {{ vlan.name }}/6 {
type ipv6_addr; flags interval
{% if prefixes6 %}
elements = { {{ prefixes6 | join(', ') }} }
{% endif %}
{% for family, family_prefixes in vrf_prefixes | groupby('family.value') %}
{% for vlan, vlan_prefixes in family_prefixes | groupby('vlan.vid') %}
set {{ vlan_prefixes[0].vlan.name }}{% if family == 6 %}/6{% endif %} {
type ipv{{ family }}_addr; flags interval
elements = { {{ vlan_prefixes | map(attribute='prefix') | join(',') }} }
}
{% endfor %}
{% endfor %}

View file

@ -80,16 +80,6 @@ table inet filter {
ct status dnat accept \
comment "Forward DNAT traffic for servers and suchlike"
# Forward IPv4 to/from VPN users in the same network.
{% for vlan in vlans %}
iif @inside ip saddr @{{ vlan.name }} ip daddr @{{ vlan.name }} accept
{% endfor %}
# Forward IPv6 to/from VPN users in the same network.
{% for vlan in vlans %}
iif @inside ip6 saddr @{{ vlan.name }}/6 ip6 daddr @{{ vlan.name }}/6 accept
{% endfor %}
include "/etc/nftables.d/forward.nft*"
}

View file

@ -1,11 +1,8 @@
{% set prefixes = query('netbox.netbox.nb_lookup', 'prefixes', raw_data=true) -%}
{
{% for vlan in vlans %}
{% set vlan_prefixes = prefixes | selectattr('vlan') | selectattr('vlan.id', '==', vlan.id) | map(attribute='prefix') %}
"{{ vlan.name }}": {
"ip": {{ vlan_prefixes | ipv4 | to_json }},
"ip6": {{ vlan_prefixes | ipv6 | to_json }}
{% for vlan, addrs in vrf_prefixes | groupby('vlan.vid') %}
"{{ addrs[0].vlan.name }}": {
"ip": {{ addrs | selectattr('family.value', '==', 4) | map(attribute='prefix') | to_json }},
"ip6": {{ addrs | selectattr('family.value', '==', 6) | map(attribute='prefix') | to_json }}
}{% if not loop.last %},{% endif +%}
{% endfor %}
}

View file

@ -78,16 +78,13 @@ route-map loopbacks permit 10
ip prefix-list default permit 0.0.0.0/0
ipv6 prefix-list default permit ::/0
{% for tenant in my_tenants %}
{% for prefix in query('netbox.netbox.nb_lookup', 'prefixes', raw_data=true, api_filter='tenant='~tenant)
| selectattr('role') | selectattr('role.slug', '==', 'bgp') | rejectattr('vlan') %}
{% for prefix in bgp_prefixes | selectattr('tenant.slug', 'in', my_tenants) %}
{% if prefix.family.value == 4 %}
ip prefix-list dc-{{ tenant }} permit {{ prefix.prefix }} ge 32
ip prefix-list dc-{{ prefix.tenant.slug }} permit {{ prefix.prefix }} ge 32
{% else %}
ipv6 prefix-list dc-{{ tenant }} permit {{ prefix.prefix }} ge 64
ipv6 prefix-list dc-{{ prefix.tenant.slug }} permit {{ prefix.prefix }} ge 64
{% endif %}
{% endfor %}
{% endfor %}
# We only announce the default route to DC servers.
route-map default->dc permit 10

View file

@ -1,3 +1,9 @@
- hosts: '*'
gather_facts: false
roles:
- facts
# Set up fabric.
- hosts: spine-*
roles:
- spine
@ -10,11 +16,13 @@
roles:
- exit
# Set up access switches.
- hosts: access-*, sw-*
gather_facts: false
roles:
- access
# Set up firewall.
- hosts: fw-*
roles:
- firewall