ADD import roles

This commit is contained in:
Shirkanesi 2024-09-15 09:41:09 +02:00
parent 4798dde38c
commit e6027e9078
36 changed files with 1129 additions and 0 deletions

176
.gitignore vendored
View File

@ -45,3 +45,179 @@ tags
.ionide
# End of https://www.toptal.com/developers/gitignore/api/ansible,vim,visualstudiocode
# Created by https://www.toptal.com/developers/gitignore/api/python
# Edit at https://www.toptal.com/developers/gitignore?templates=python
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
### Python Patch ###
# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
poetry.toml
# ruff
.ruff_cache/
# LSP config files
pyrightconfig.json
# End of https://www.toptal.com/developers/gitignore/api/python

View File

@ -0,0 +1,3 @@
bird_enabled: false
bird_bgp_advertised_networks: []
bird_bgp_neighbors: []

View File

@ -0,0 +1,5 @@
---
- name: Reconfigure bird
shell: /usr/sbin/birdc configure
- name: Reload sysctl
ansible.builtin.command: sysctl --system

62
roles/bird/tasks/main.yml Normal file
View File

@ -0,0 +1,62 @@
---
- name: Set OS dependent variables
include_vars: "{{ lookup('first_found', params) }}"
vars:
params:
files:
- "{{ ansible_distribution | lower }}_{{ ansible_distribution_version | lower }}.yml"
- "{{ ansible_distribution | lower }}_{{ ansible_distribution_major_version | lower }}.yml"
- "{{ ansible_distribution | lower }}.yml"
- "{{ ansible_os_family | lower }}.yml"
- "{{ ansible_system | lower }}.yml"
paths:
- "{{ role_path }}/vars"
ignore_errors: True
tags:
- always
- name: OS is supported
assert:
that: __os_supported
quiet: True
vars:
__os_supported: "{{ lookup('vars', '{}_os_supported'.format(role_name)) | bool }}"
tags:
- always
- name: Install required packages
package:
name: "{{ item }}"
state: present
with_items: "{{ lookup('vars', '{}_packages'.format(role_name)) | list }}"
tags:
- install
- name: Enable routing in sysctl
template:
src: sysctl.conf.j2
dest: /etc/sysctl.d/90-routing.conf
owner: root
group: root
mode: "0644"
notify: Reload sysctl
- name: Install bird configuration
template:
src: bird.conf.j2
dest: "/etc/bird/bird.conf"
owner: root
group: root
mode: 0755
tags:
- configure
no_log: false
when: bird_enabled
notify: Reconfigure bird
- name: Enable bird service
ansible.builtin.systemd_service:
name: bird.service
state: "{{ 'started' if bird_enabled else 'stopped' }}"
enabled: "{{ bird_enabled }}"
when: not ansible_check_mode

View File

@ -0,0 +1,261 @@
# {{ ansible_managed }}
router id {{ bird_router_id }};
protocol device {
scan time 10;
}
protocol kernel {
scan time 5;
ipv6 {
import none;
export filter {
{% if bird_prefsrc is defined %}
krt_prefsrc = {{ bird_prefsrc }};
{% endif %}
if source = RTS_BGP then accept;
if source = RTS_STATIC then accept;
if source = RTS_DEVICE then reject;
accept;
};
};
}
protocol kernel {
scan time 5;
ipv4 {
import none;
export filter {
{% if bird_prefsrc_v4 is defined %}
krt_prefsrc = {{ bird_prefsrc_v4 }};
{% endif %}
if source = RTS_BGP then accept;
if source = RTS_STATIC then accept;
if source = RTS_DEVICE then reject;
accept;
};
};
}
protocol static {
{% for net in bird_bgp_advertised_networks %}
route {{ net }} reject;
{% endfor %}
ipv6 {
import all;
export none;
};
}
protocol static {
{% for net in bird_bgp_advertised_networks_v4 | default([]) %}
route {{ net }} reject;
{% endfor %}
ipv4 {
import all;
export none;
};
}
{%if bird_ospf_interfaces | default([]) | length > 0%}
############################################
# _|_| _|_|_| _|_|_| _|_|_|_| #
# _| _| _| _| _| _| #
# _| _| _|_| _|_|_| _|_|_| #
# _| _| _| _| _| #
# _|_| _|_|_| _| _| #
############################################
# Filter to export all direct and bgp routes into OSPF
filter export_all_OSPF {
if ( source = RTS_DEVICE ) then accept;
if ( source = RTS_STATIC_DEVICE ) then accept;
if ( source = RTS_STATIC ) then accept;
if ( source = RTS_BGP ) then accept;
reject;
}
protocol ospf v3 ospf6 {
ipv6 {
{% if bird_ospf_export_all | default(false) %}
export filter export_all_OSPF;
{% endif %}
};
area 0 {
{%for iface in bird_ospf_interfaces%}
interface "{{ iface.name }}" {
{%if iface.stub | default(false)%}
stub;
{%endif%}
};
{%endfor%}
};
}
{%endif%}
{% if bird_bgp_neighbors | default([]) | length > 0%}
###################################
# _|_|_| _|_|_| _|_|_| #
# _| _| _| _| _| #
# _|_|_| _| _|_| _|_|_| #
# _| _| _| _| _| #
# _|_|_| _|_|_| _| #
###################################
define OWNNETSET = [{{ bird_bgp_advertised_networks | join(', ') }}];
define OWNNETSET_v4 = [{{ bird_bgp_advertised_networks_v4 | default([]) | join(', ') }}];
define OWNASN = {{ bird_bgp_local_asn }};
function is_self_net() {
if net ~ OWNNETSET then return true;
if net ~ OWNNETSET_v4 then return true;
return false;
}
function has_private_asn() {
# Check if the AS path contains any private ASN
if (bgp_path ~ [64512..65534]) || (bgp_path ~ [4200000000..4294967294]) then {
print "Rejecting route with private ASN in AS path: ", bgp_path;
return true;
}
return false;
}
function is_reserved_net() {
# ULA
if net ~ fc00::/7 then return true;
# Link-local
if net ~ fe80::/10 then return true;
# Documentation
if net ~ 2001:db8::/32 then return true;
# Discard
if net ~ 100::/64 then return true;
# Orchid
if net ~ 2001:20::/28 then return true;
# IPv4
if net ~ 0.0.0.0/8 then return true;
if net ~ 10.0.0.0/8 then return true;
if net ~ 100.64.0.0/10 then return true;
if net ~ 127.0.0.0/8 then return true;
if net ~ 169.254.0.0/16 then return true;
if net ~ 172.16.0.0/12 then return true;
if net ~ 192.0.0.0/24 then return true;
if net ~ 192.0.2.0/24 then return true;
if net ~ 192.88.99.0/24 then return true;
if net ~ 192.168.0.0/16 then return true;
if net ~ 198.18.0.0/15 then return true;
if net ~ 198.51.100.0/24 then return true;
if net ~ 203.0.113.0/24 then return true;
if net ~ 224.0.0.0/4 then return true;
if net ~ 233.252.0.0/24 then return true;
if net ~ 240.0.0.0/4 then return true;
if net ~ 255.255.255.255/32 then return true;
return false;
}
{%for peer in bird_bgp_neighbors%}
# {{ peer.name }}
{% set peer_name = peer.name | lower | regex_replace('[\s\.\-\$%&/()=]', '_') %}
filter {{ peer_name }}_BGP_IMPORT {
# don't re-import networks announced by ourself
if is_self_net() then reject;
{% if not (peer.allow_reserved_networks | default(bird_bgp_allow_reserved_networks | default(false))) %}
# don't import reserved networks
if is_reserved_net() then reject;
{% endif %}
# never accept routes more specific than /{{ peer.min_pref_len | default(bird_bgp_min_pref_len | default(48)) }}
if net.len > {{ peer.min_pref_len | default(bird_bgp_min_pref_len | default(48)) }} then reject;
{% if not (peer.allow_private_asn | default(bird_bgp_allow_private_asn | default(false))) %}
# prevent importing paths with private ASNs
if has_private_asn() then reject;
{% endif %}
{% if (peer.max_as_path_length is defined) or (bird_bgp_max_as_path_length is defined) %}
if bgp_path.len > {{ peer.max_as_path_length | default(bird_bgp_max_as_path_length) }} then reject;
{% endif %}
{% if (peer.restrict_prefixes | default([]) | length > 0) %}
if net !~ [{{ peer.restrict_prefixes | join(', ') }}] then reject;
{% endif %}
accept;
}
# export rules for {{ peer_name }}
filter {{ peer_name }}_ALLOWED_BGP {
{% for n in range(peer.as_prepend | default(1)) %}
bgp_path.prepend({{ peer.local_asn | default('OWNASN') }});
{% endfor %}
{% if not (peer.allow_reserved_networks | default(bird_bgp_allow_reserved_networks | default(false))) %}
# don't export reserved networks
if is_reserved_net() then reject;
{% endif %}
# never send routes more specific than /{{ peer.min_pref_len | default(bird_bgp_min_pref_len | default(48)) }}
if net.len > {{ peer.min_pref_len | default(bird_bgp_min_pref_len | default(48)) }} then reject;
{% if not (peer.allow_private_asn | default(bird_bgp_allow_private_asn | default(false))) %}
# prevent exposing paths with private ASNs
if has_private_asn() then reject;
{% endif %}
if is_self_net() then accept;
{% if (peer.redistribute_learned | default(false)) %}
# redistribute all other routes learned from BGP
if source = RTS_BGP then {
{% if not (peer.redistribute_reserved | default(false)) %}
# don't redistribute reserved networks
if is_reserved_net() then reject;
{% endif %}
{% for n in range(peer.redistribute_prepend | default(0)) %}
bgp_path.prepend({{ peer.local_asn | default('OWNASN') }});
{% endfor %}
accept;
}
{% endif %}
reject;
}
protocol bgp {{ peer_name }} {
local {{ peer.local_ip }} as {{ peer.local_asn | default('OWNASN') }};
neighbor {{ peer.neighbor_ip }} as {{ peer.neighbor_asn }};
description "{{ peer.description | default(peer.name) }}";
{% if peer.password is defined %}
password "{{ peer.password }}";
{% endif %}
{% if peer.metric is defined %}
path metric {{ peer.metric }};
{% endif %}
{% if peer.ipv4 | default(false) == true %}
ipv4 {
import filter {{ peer_name }}_BGP_IMPORT;
export filter {{ peer_name }}_ALLOWED_BGP;
{% if peer.import_limit | default(1000) != false %}
import limit {{ peer.import_limit | default(1000) }} action block;
{% endif %}
next hop {{ ('address ' + peer.next_hop) if peer.next_hop is defined else 'self' }};
};
{% else %}
ipv6 {
import filter {{ peer_name }}_BGP_IMPORT;
export filter {{ peer_name }}_ALLOWED_BGP;
{% if peer.import_limit | default(1000) != false %}
import limit {{ peer.import_limit | default(1000) }} action block;
{% endif %}
next hop {{ ('address ' + peer.next_hop) if peer.next_hop is defined else 'self' }};
};
{% endif %}
}
{%endfor%}
# TODO: roa with routinator...
# roa4 table dn42_roa;
# protocol static {
# roa4 { table dn42_roa; };
# include "/etc/bird/roa_dn42.conf";
# };
{% endif %}

View File

@ -0,0 +1,2 @@
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1

View File

@ -0,0 +1,3 @@
bird_os_supported: true
bird_packages:
- bird2

1
roles/bird/vars/main.yml Normal file
View File

@ -0,0 +1 @@
bird_os_supported: false

View File

@ -0,0 +1,2 @@
net_interfaces: []
interfaces_manage_ifupdown: true

View File

@ -0,0 +1,12 @@
---
- name: Reload udev
# TODO! FIXME!
systemd_service:
name: systemd-udev-trigger.service
state: restarted
- name: Reload ifupdown
# TODO! FIXME! This drops some packages. Can this be done better?
systemd_service:
name: networking.service
state: restarted
throttle: 1

View File

@ -0,0 +1,57 @@
---
- name: Set OS dependent variables
include_vars: "{{ lookup('first_found', params) }}"
vars:
params:
files:
- "{{ ansible_distribution | lower }}_{{ ansible_distribution_version | lower }}.yml"
- "{{ ansible_distribution | lower }}_{{ ansible_distribution_major_version | lower }}.yml"
- "{{ ansible_distribution | lower }}.yml"
- "{{ ansible_os_family | lower }}.yml"
- "{{ ansible_system | lower }}.yml"
paths:
- "{{ role_path }}/vars"
ignore_errors: True
tags:
- always
- name: OS is supported
assert:
that: __os_supported
quiet: True
vars:
__os_supported: "{{ lookup('vars', '{}_os_supported'.format(role_name)) | bool }}"
tags:
- always
- name: Install required packages
package:
name: "{{ item }}"
state: present
with_items: "{{ lookup('vars', '{}_packages'.format(role_name)) | list }}"
tags:
- install
- name: Install persistent if-names
template:
src: 60-persistent-net.rules.j2
dest: "/etc/udev/rules.d/60-persistent-net.rules"
owner: root
group: root
mode: 0644
tags:
- configure
no_log: false
notify: Reload udev
- name: Install ifupdown config
template:
src: ifupdown2.j2
dest: "/etc/network/interfaces.d/ansible"
owner: root
group: root
mode: 0644
tags:
- configure
no_log: false
notify: Reload ifupdown

View File

@ -0,0 +1,5 @@
{% for iface in net_interfaces %}
{% if iface.name is defined and iface.mac is defined %}
SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ATTR{address}=="{{ iface.mac }}", ATTR{dev_id}=="0x0", ATTR{type}=="1", KERNEL=="e*", NAME="{{ iface.name }}"
{% endif %}
{% endfor %}

View File

@ -0,0 +1,43 @@
# {{ ansible_managed }}
{% macro iface_config(iface, type, inet) %}
iface {{ iface.name }} {{ type }} {{ inet.conf | default('manual') }}
{% if iface.vlan_raw_device is defined %}
vlan-raw-device {{ iface.vlan_raw_device }}
{% endif %}
{% if iface.dummy | default(false) %}
pre-up ip link show $IFACE > /dev/null || sudo ip link add $IFACE type dummy
post-down ip link show $IFACE > /dev/null && sudo ip link del $IFACE
{% endif %}
{% for addr in inet.addr %}
up ip addr add {{ addr }} dev $IFACE
pre-down ip addr del {{ addr }} dev $IFACE
{% endfor %}
{% if inet.gateway is defined and inet.gateway %}
up ip r add default via {{ inet.gateway }} dev $IFACE
pre-down ip r del default via {{ inet.gateway }} dev $IFACE
{% endif %}
{% if inet.routes is defined and inet.routes %}
{% for route in inet.routes %}
post-up ip r add {{ route.dst }} via {{ route.via }} dev $IFACE
pre-down ip r del {{ route.dst }} via {{ route.via }} dev $IFACE
{% endfor %}
{% endif %}
{% if inet.custom is defined and inet.custom %}
{{ inet.custom | default('') | indent(4) }}
{% endif %}
{% endmacro %}
{% for iface in net_interfaces %}
{% if interfaces_manage_ifupdown or (iface.ifupdown | default(false)) %}
# {{ iface.name }}
allow-hotplug {{ iface.name }}
{% if iface.inet is defined and iface.inet %}
{{ iface_config(iface, 'inet', iface.inet) }}
{% endif %}
{% if iface.inet6 is defined and iface.inet6 %}
{{ iface_config(iface, 'inet6', iface.inet6) }}
{% endif %}
{% endif %}
{% endfor %}

View File

@ -0,0 +1,5 @@
interfaces_os_supported: true
interfaces_packages:
- ifupdown
- udev
- vlan

View File

@ -0,0 +1 @@
interfaces_os_supported: false

View File

@ -0,0 +1 @@
isc_dhcp_enabled: false

View File

@ -0,0 +1,6 @@
---
- name: Restart isc_dhcp
ansible.builtin.systemd_service:
name: isc-dhcp-server.service
state: restarted
enabled: true

View File

@ -0,0 +1,64 @@
---
- name: Set OS dependent variables
include_vars: "{{ lookup('first_found', params) }}"
vars:
params:
files:
- "{{ ansible_distribution | lower }}_{{ ansible_distribution_version | lower }}.yml"
- "{{ ansible_distribution | lower }}_{{ ansible_distribution_major_version | lower }}.yml"
- "{{ ansible_distribution | lower }}.yml"
- "{{ ansible_os_family | lower }}.yml"
- "{{ ansible_system | lower }}.yml"
paths:
- "{{ role_path }}/vars"
ignore_errors: True
tags:
- always
- name: OS is supported
assert:
that: __os_supported
quiet: True
vars:
__os_supported: "{{ lookup('vars', '{}_os_supported'.format(role_name)) | bool }}"
tags:
- always
- name: Install required packages
package:
name: "{{ item }}"
state: present
with_items: "{{ lookup('vars', '{}_packages'.format(role_name)) | list }}"
when: radvd_enabled
tags:
- install
- name: Install dhcpd configuration
template:
src: dhcpd.conf.j2
dest: "/etc/dhcp/dhcpd.conf"
owner: root
group: root
mode: 0644
tags:
- configure
no_log: false
notify: Restart isc_dhcp
- name: Install dhcpd configuration
template:
src: isc-dhcp-server.j2
dest: "/etc/default/isc-dhcp-server"
owner: root
group: root
mode: 0644
tags:
- configure
no_log: false
notify: Restart isc_dhcp
- name: Enable dhcpd service
ansible.builtin.systemd_service:
name: isc-dhcp-server.service
state: started
enabled: true

View File

@ -0,0 +1,21 @@
# {{ ansible_managed }}
ddns-update-style none;
authoritative;
{% for net in isc_dhcp_networks | default([]) %}
# {{ net.interface }}
subnet {{ net.subnet }} netmask {{ net.netmask }} {
interface {{ net.interface }};
option routers {{ net.router }};
option subnet-mask {{ net.netmask }};
{% if net.dns is defined %}
option domain-name-servers {{ net.dns }};
{% endif %}
option domain-name "{{ net.domain | default('hiwi.net.scc.kit.edu') }}";
range {{ net.range_start }} {{ net.range_end }};
default-lease-time 600;
max-lease-time 7200;
}
{% endfor %}

View File

@ -0,0 +1,2 @@
# {{ ansible_managed }}
INTERFACESv4="{{ isc_dhcp_networks | map(attribute='interface') | join(' ') }}"

View File

@ -0,0 +1,3 @@
isc_dhcp_os_supported: true
isc_dhcp_packages:
- isc-dhcp-server

View File

@ -0,0 +1 @@
isc_dhcp_os_supported: false

View File

@ -0,0 +1,2 @@
keepalived_enabled: false
keepalived_interfaces: []

View File

@ -0,0 +1,9 @@
---
- name: Restart keepalived
systemd_service:
name: keepalived.service
state: restarted
enabled: true
when: not ansible_check_mode
- name: Reload sysctl
ansible.builtin.command: sysctl --system

View File

@ -0,0 +1,52 @@
---
- name: Set OS dependent variables
include_vars: "{{ lookup('first_found', params) }}"
vars:
params:
files:
- "{{ ansible_distribution | lower }}_{{ ansible_distribution_version | lower }}.yml"
- "{{ ansible_distribution | lower }}_{{ ansible_distribution_major_version | lower }}.yml"
- "{{ ansible_distribution | lower }}.yml"
- "{{ ansible_os_family | lower }}.yml"
- "{{ ansible_system | lower }}.yml"
paths:
- "{{ role_path }}/vars"
ignore_errors: True
tags:
- always
- name: OS is supported
assert:
that: __os_supported
quiet: True
vars:
__os_supported: "{{ lookup('vars', '{}_os_supported'.format(role_name)) | bool }}"
tags:
- always
- name: Install required packages
package:
name: "{{ item }}"
state: present
with_items: "{{ lookup('vars', '{}_packages'.format(role_name)) | list }}"
tags:
- install
- name: Install keepalived configuration
template:
src: keepalived.conf.j2
dest: "/etc/keepalived/keepalived.conf"
owner: root
group: root
mode: 0644
tags:
- configure
no_log: false
notify: Restart keepalived
- name: Enable keepalived service
ansible.builtin.systemd_service:
name: keepalived.service
state: "{{ 'started' if keepalived_enabled else 'stopped' }}"
enabled: "{{ keepalived_enabled }}"
when: not ansible_check_mode

View File

@ -0,0 +1,27 @@
# {{ ansible_managed }}
vrrp_sync_group G1 {
group {
{%for iface in keepalived_interfaces %}
{{ iface.name }}
{% endfor %}
}
}
{%for iface in keepalived_interfaces %}
vrrp_instance {{ iface.name }} {
state {{ keepalived_init_state | default('BACKUP') }}
interface {{ iface.interface | default(iface.name) }}
virtual_router_id 42
priority {{ keepalived_host_prio }}
advert_int 2
authentication {
auth_type PASS
auth_pass {{ iface.password | default(keepalived_password) }}
}
virtual_ipaddress {
{% for addr in iface.addresses %}
{{ addr }}
{% endfor %}
}
}
{% endfor %}

View File

@ -0,0 +1,3 @@
keepalived_os_supported: true
keepalived_packages:
- keepalived

View File

@ -0,0 +1 @@
keepalived_os_supported: false

View File

@ -0,0 +1 @@
radvd_enabled: false

View File

@ -0,0 +1,6 @@
---
- name: Restart radvd
ansible.builtin.systemd_service:
name: radvd.service
state: restarted
enabled: true

View File

@ -0,0 +1,52 @@
---
- name: Set OS dependent variables
include_vars: "{{ lookup('first_found', params) }}"
vars:
params:
files:
- "{{ ansible_distribution | lower }}_{{ ansible_distribution_version | lower }}.yml"
- "{{ ansible_distribution | lower }}_{{ ansible_distribution_major_version | lower }}.yml"
- "{{ ansible_distribution | lower }}.yml"
- "{{ ansible_os_family | lower }}.yml"
- "{{ ansible_system | lower }}.yml"
paths:
- "{{ role_path }}/vars"
ignore_errors: True
tags:
- always
- name: OS is supported
assert:
that: __os_supported
quiet: True
vars:
__os_supported: "{{ lookup('vars', '{}_os_supported'.format(role_name)) | bool }}"
tags:
- always
- name: Install required packages
package:
name: "{{ item }}"
state: present
with_items: "{{ lookup('vars', '{}_packages'.format(role_name)) | list }}"
when: radvd_enabled
tags:
- install
- name: Install radvd configuration
template:
src: radvd.conf.j2
dest: "/etc/radvd.conf"
owner: root
group: root
mode: 0644
tags:
- configure
no_log: false
notify: Restart radvd
- name: Enable radvd service
ansible.builtin.systemd_service:
name: radvd.service
state: started
enabled: true

View File

@ -0,0 +1,24 @@
# {{ ansible_managed }}
{% for iface in radvd_interfaces | default([]) %}
interface {{ iface.name }} {
AdvSendAdvert on;
MinRtrAdvInterval 5;
MaxRtrAdvInterval 15;
route ::/0 {};
AdvManagedFlag off;
AdvOtherConfigFlag off;
prefix {{ iface.prefix }} {
AdvOnLink on;
AdvAutonomous on;
AdvRouterAddr on;
};
{% if iface.source is defined %}
AdvRASrcAddress {
{{ iface.source }};
};
{% endif %}
RDNSS {{ iface.dns | default(['2a00:1398::1', '2a00:1398::2']) | join(' ') }} {
AdvRDNSSLifetime {{ iface.dns_lifetime | default(3600) }};
};
};
{% endfor %}

View File

@ -0,0 +1,3 @@
radvd_os_supported: true
radvd_packages:
- radvd

View File

@ -0,0 +1 @@
bird_os_supported: false

View File

@ -0,0 +1,193 @@
import ipaddress
class FilterModule(object):
def filters(self):
return {
'generate_radvd': self.generate_radvd,
'generate_keepalived': self.generate_keepalived,
'generate_interfaces': self.generate_interfaces,
'generate_nftables': self.generate_nftables,
'generate_dhcpd': self.generate_dhcpd,
'generate_ospf': self.generate_ospf,
}
def generate_radvd(self, cluster_networks):
result = []
for net in cluster_networks:
if 'prefix' not in net:
continue
if 'dns' in net:
dns = net['dns']
else:
dns = ['2a00:1398::64:1', '2a00:1398::64:2']
radvd = {
'name': net['interface'],
'prefix': net['prefix'],
'source': 'fe80::1',
'dns': dns,
}
result.append(radvd)
return result
def generate_keepalived(self, cluster_networks):
result = []
for net in cluster_networks:
keepalived = {
'name': net['interface'],
'addresses': ['fe80::1/64'],
}
result.append(keepalived)
if 'prefix_v4' in net:
keepalived = {
'name': net['interface'] + '_v4',
'interface': net['interface'],
'addresses': [self.nth_addr_v4(net['prefix_v4'], 1)],
}
result.append(keepalived)
return result
def nth_addr(self, prefix: str, n:int) -> str:
network = ipaddress.IPv6Network(prefix)
# Check if n is within the valid range
if n >= network.num_addresses or n < 0:
raise ValueError(f"n must be between 0 and {network.num_addresses - 1}")
# Calculate the n-th address
nth_address = network.network_address + n
return str(nth_address) + '/' + str(network.prefixlen)
def nth_addr_v4(self, prefix: str, n:int, with_prefix: bool = True) -> str:
network = ipaddress.IPv4Network(prefix)
# Check if n is within the valid range
if n >= network.num_addresses or n < 0:
raise ValueError(f"n must be between 0 and {network.num_addresses - 1}")
# Calculate the n-th address
nth_address = network.network_address + n
if with_prefix:
return str(nth_address) + '/' + str(network.prefixlen)
else:
return str(nth_address)
def generate_interfaces(self, cluster_networks, addr_num):
result = []
for net in cluster_networks:
interface = {
'name': net['interface'],
'mac': net['mac'],
}
if 'prefix' in net:
inet6 = {
'addr': [self.nth_addr(net['prefix'], addr_num)],
'gateway': False,
'custom': False,
'routes': False,
}
interface['inet6'] = inet6
if 'prefix_v4' in net:
inet4 = {
'addr': [self.nth_addr_v4(net['prefix_v4'], addr_num)],
'gateway': False,
'custom': False,
'routes': False,
}
interface['inet'] = inet4
result.append(interface)
return result
def clean_port_range(self, port_range):
if ('{' not in port_range) and ('-' in port_range or ',' in port_range):
return '{{' + port_range + '}}'
return port_range
def generate_nftables(self, cluster_networks, upstream_interface):
result = []
nat_rules = []
default_policies = []
for net in cluster_networks:
if 'prefix' in net:
if net.get('firewall', None) is not None and net['firewall'].get('enabled', False) == True:
for rule in net['firewall'].get('rules', []):
if rule.get('dst', None) is None:
rule['dst'] = '%net'
nft_rule = 'oifname {} '.format(net['interface'])
if rule.get('src', '') != '':
nft_rule += 'ip6 saddr {} '.format(rule['src'].replace('%net', net['prefix']))
if rule.get('dst', '') != '':
nft_rule += 'ip6 daddr {} '.format(rule['dst'].replace('%net', net['prefix']))
if rule.get('sport', '') != '':
nft_rule += '{{proto}} sport {} '.format(self.clean_port_range(rule['sport']))
if rule.get('dport', '') != '':
nft_rule += '{{proto}} dport {} '.format(self.clean_port_range(rule['dport']))
nft_rule += rule['action']
for proto in rule.get('protocols', ['tcp', 'udp']):
result.append(nft_rule.format(proto=proto))
default_policies.append('oifname {} ip6 daddr {} {}'.format(net['interface'], net['prefix'], net['firewall']['policy']))
else:
default_policies.append('oifname {} ip6 daddr {} {}'.format(net['interface'], net['prefix'], 'accept'))
# NAT
if net.get('nat', None) is not None and net['nat'].get('enabled', False) == True:
if net['nat'].get('source', None) is not None and net.get('prefix_v4', None) is not None:
nat_rules.append('ip saddr {prefix} oif {dest_iface} snat to {source}'.format(prefix=net['prefix_v4'], dest_iface=upstream_interface, source=net['nat']['source']))
default_policies.append('oifname {} accept'.format(upstream_interface))
full_rules = "\n".join(result + default_policies)
full_nat = "\n".join(nat_rules)
ret = [
{
'name': 'cluster_router_bundle',
'rule': full_rules,
'chain': 'forward',
},
{
'name': 'cluster_router_bundle',
'rule': full_nat,
'chain': 'postrouting',
}
]
return ret
def is_v4(self, addr: str) -> bool:
try:
ipaddress.IPv4Address(addr)
return True
except ipaddress.AddressValueError:
return False
def generate_dhcpd(self, cluster_networks):
result = []
for net in cluster_networks:
if net.get('prefix_v4', None) is None:
continue
if net.get('dhcp', None) is None or net['dhcp'].get('enabled', False) == False:
continue
router = self.nth_addr_v4(net['prefix_v4'], 1, False)
subnet = self.nth_addr_v4(net['prefix_v4'], 0, False)
netmask = str(ipaddress.IPv4Network(net['prefix_v4']).netmask)
range_start = self.nth_addr_v4(net['prefix_v4'], net['dhcp'].get('range_start', 20), False)
range_end = self.nth_addr_v4(net['prefix_v4'], net['dhcp'].get('range_end', 150), False)
dhcpd = {
'interface': net['interface'],
'subnet': subnet,
'router': router,
'netmask': netmask,
'range_start': range_start,
'range_end': range_end,
}
dns = ', '.join(list(filter(lambda x: self.is_v4(x), net.get('dns', []))))
if len(dns) > 0:
dhcpd['dns'] = dns
result.append(dhcpd)
return result
def generate_ospf(self, cluster_networks):
result = []
for net in cluster_networks:
ospf = {
'name': net['interface'],
'stub': True,
}
result.append(ospf)
return result

View File

@ -0,0 +1,19 @@
---
- name: Generate radvd config
set_fact:
radvd_interfaces: "{{ cluster_networks | generate_radvd | union(radvd_interfaces | default([])) }}"
- name: Generate keepalived config
set_fact:
keepalived_interfaces: "{{ cluster_networks | generate_keepalived | union(keepalived_interfaces | default([])) }}"
- name: Generate interfaces config
set_fact:
net_interfaces: "{{ cluster_networks | generate_interfaces(cluster_v6_end) | union(net_interfaces | default([])) }}"
- name: Generate firewall config
set_fact:
nftables_rules: "{{ cluster_networks | generate_nftables(cluster_upstream_interface) | union(nftables_rules | default([])) }}"
- name: Generate dhcp config
set_fact:
isc_dhcp_networks: "{{ cluster_networks | generate_dhcpd | union(isc_dhcp_networks | default([])) }}"
# - name: Generate ospf config
# set_fact:
# bird_ospf_interfaces: "{{ cluster_networks | generate_ospf | union(bird_ospf_interfaces | default([])) }}"