Compare commits

...

3 commits

Author SHA1 Message Date
Timotej Lazar 29598ef4bb Rework service handling
Allow running playbooks without NetBox access. Mainly to bootstrap
NetBox itself.

Would prefer not to access network from filter plugins, so maybe do
that at some point also.
2024-06-19 13:33:32 +02:00
Timotej Lazar 38c3464279 alpine: assume one DNS name per host
Avoid needless complexity.
2024-06-19 13:14:51 +02:00
Timotej Lazar 393614aa79 alpine: configure unattended upgrades 2024-06-17 09:52:56 +02:00
13 changed files with 70 additions and 49 deletions

View file

@ -7,7 +7,8 @@ class FilterModule(object):
'''Various utilities for manipulating NetBox data''' '''Various utilities for manipulating NetBox data'''
def __init__(self): def __init__(self):
self.nb = pynetbox.api(os.getenv('NETBOX_API'), os.getenv('NETBOX_TOKEN')) if 'NETBOX_API' in os.environ and 'NETBOX_TOKEN' in os.environ:
self.nb = pynetbox.api(os.getenv('NETBOX_API'), os.getenv('NETBOX_TOKEN'))
def filters(self): def filters(self):
return { return {
@ -37,11 +38,12 @@ class FilterModule(object):
def allowed_prefixes(self, service): def allowed_prefixes(self, service):
'''Return a list of allowed IP prefixes for the given service''' '''Return a list of allowed IP prefixes for the given service'''
service_data = self.nb.ipam.services.get(service['id']).custom_fields if 'custom_fields' in service:
if service_data['allowed_prefixes']: service = service['custom_fields']
yield from self.nb.ipam.prefixes.filter(id=[prefix['id'] for prefix in service_data['allowed_prefixes']]) if prefixes := service.get('allowed_prefixes'):
if service_data['allowed_vlans']: yield from self.nb.ipam.prefixes.filter(id=[prefix['id'] for prefix in prefixes])
yield from self.nb.ipam.prefixes.filter(vlan_id=[vlan['id'] for vlan in service_data['allowed_vlans']]) if vlans := service.get('allowed_vlans'):
if service_data['allowed_clusters']: yield from self.nb.ipam.prefixes.filter(vlan_id=[vlan['id'] for vlan in vlans])
for device in self.nb.dcim.devices.filter(cluster_id=[cluster['id'] for cluster in service_data['allowed_clusters']]): if clusters := service.get('allowed_clusters'):
for device in self.nb.dcim.devices.filter(cluster_id=[cluster['id'] for cluster in clusters]):
yield from self.nb.ipam.ip_addresses.filter(role='loopback', device_id=device.id) yield from self.nb.ipam.ip_addresses.filter(role='loopback', device_id=device.id)

View file

@ -0,0 +1,8 @@
#!/bin/sh
upgrade() {
echo "Starting upgrade on $(date)"
apk upgrade --update
}
upgrade >> /var/log/unattended-upgrade.log

View file

@ -0,0 +1,3 @@
/var/log/unattended-upgrade.log {
missingok
}

View file

@ -22,6 +22,7 @@
name: name:
- git - git
- iproute2 - iproute2
- logrotate
- nftables - nftables
- procps - procps
- rsync - rsync
@ -64,3 +65,15 @@
name: qemu-guest-agent name: qemu-guest-agent
enabled: yes enabled: yes
state: started state: started
- name: Install automatic upgrade script
copy:
dest: /etc/periodic/weekly/
src: unattended-upgrade
mode: 0755
- name: Configure log rotation for automatic upgrades
copy:
dest: /etc/logrotate.d/unattended-upgrade
src: unattended-upgrade.logrotate
mode: 0644

View file

@ -7,7 +7,9 @@ table inet filter {
{% set prefixes4 = prefixes | selectattr('family.value', '==', 4) | map('string') %} {% set prefixes4 = prefixes | selectattr('family.value', '==', 4) | map('string') %}
{% set prefixes6 = prefixes | selectattr('family.value', '==', 6) | map('string') %} {% set prefixes6 = prefixes | selectattr('family.value', '==', 6) | map('string') %}
{% set ports = service.ports | compact_numlist %} {% set ports = service.ports | compact_numlist %}
{% if 'name' in service %}
# service {{ service.name }} # service {{ service.name }}
{% endif %}
{% if prefixes4 or prefixes6 %} {% if prefixes4 or prefixes6 %}
{% if prefixes4 %} {% if prefixes4 %}
ip saddr { {{ prefixes4 | join(', ') }} } tcp dport { {{ ports }} } accept ip saddr { {{ prefixes4 | join(', ') }} } tcp dport { {{ ports }} } accept

View file

@ -54,7 +54,7 @@ table inet filter {
ip saddr @allowed accept # TODO remove exceptions ip saddr @allowed accept # TODO remove exceptions
ip6 saddr @allowed/6 accept # TODO remove exceptions ip6 saddr @allowed/6 accept # TODO remove exceptions
{% for service in cluster.custom_fields.services %} {% for service in cluster_services %}
{% set prefixes = service | allowed_prefixes %} {% set prefixes = service | allowed_prefixes %}
{% set prefixes4 = prefixes | selectattr('family.value', '==', 4) | map('string') %} {% set prefixes4 = prefixes | selectattr('family.value', '==', 4) | map('string') %}
{% set prefixes6 = prefixes | selectattr('family.value', '==', 6) | map('string') %} {% set prefixes6 = prefixes | selectattr('family.value', '==', 6) | map('string') %}

View file

@ -1,11 +1,10 @@
{% for fqdn in fqdns %}
server { server {
listen 443 ssl http2; listen 443 ssl http2;
listen [::]:443 ssl http2; listen [::]:443 ssl http2;
server_name {{ fqdn }}; server_name {{ dns_name }};
ssl_certificate /etc/letsencrypt/live/{{ fqdn }}/fullchain.pem; ssl_certificate /etc/letsencrypt/live/{{ dns_name }}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/{{ fqdn }}/privkey.pem; ssl_certificate_key /etc/letsencrypt/live/{{ dns_name }}/privkey.pem;
client_max_body_size 100M; client_max_body_size 100M;
@ -35,5 +34,3 @@ server {
fastcgi_pass unix:/run/php-fpm.socket; fastcgi_pass unix:/run/php-fpm.socket;
} }
} }
{% endfor %}

View file

@ -1,18 +1,21 @@
# Make expensive lookups to NetBox once for later reference by any host. # Make expensive lookups to NetBox once for later reference by any host.
- name: Lookup networks and prefixes - when: lookup("env", "NETBOX_API") != ""
set_fact: block:
vlans: '{{ query("netbox.netbox.nb_lookup", "vlans", api_filter="group=new-net", raw_data=true) - name: Lookup networks and prefixes
| sort(attribute="vid") }}' set_fact:
prefixes: '{{ query("netbox.netbox.nb_lookup", "prefixes", raw_data=true) vlans: '{{ query("netbox.netbox.nb_lookup", "vlans", api_filter="group=new-net", raw_data=true)
| sort(attribute="prefix") | sort(attribute="family.value") }}' | sort(attribute="vid") }}'
prefixes: '{{ query("netbox.netbox.nb_lookup", "prefixes", raw_data=true)
| sort(attribute="prefix") | sort(attribute="family.value") }}'
- name: Get my cluster and all nodes in it - when: 'cluster is defined'
set_fact: block:
cluster: '{{ query("netbox.netbox.nb_lookup", "clusters", raw_data=true, api_filter="name="+cluster) | first }}' - name: Get my cluster and all nodes in it
nodes: '{{ groups["cluster_"+cluster] | map("extract", hostvars) | rejectattr("is_virtual") }}' set_fact:
when: cluster cluster: '{{ query("netbox.netbox.nb_lookup", "clusters", raw_data=true, api_filter="name="+cluster) | first }}'
nodes: '{{ groups["cluster_"+cluster] | map("extract", hostvars) | rejectattr("is_virtual") }}'
- name: Get my domain names if any - name: Get cluster services
set_fact: set_fact:
fqdns: '{{ interfaces | map(attribute="ip_addresses") | flatten cluster_services: '{{ (cluster_services|default([])) + query("netbox.netbox.nb_lookup", "services", raw_data=true, api_filter="id="+item) }}'
| map(attribute="dns_name") | reject("==", "") | sort | unique }}' loop: '{{ cluster.custom_fields.services | map(attribute="id") | map("string") }}'

View file

@ -1,10 +1,9 @@
{% for fqdn in fqdns %}
server { server {
server_name {{ fqdn }}; server_name {{ dns_name }};
listen [::]:443 ssl ipv6only=off; listen [::]:443 ssl ipv6only=off;
ssl_certificate /etc/letsencrypt/live/{{ fqdn }}/fullchain.pem; ssl_certificate /etc/letsencrypt/live/{{ dns_name }}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/{{ fqdn }}/privkey.pem; ssl_certificate_key /etc/letsencrypt/live/{{ dns_name }}/privkey.pem;
location / { location / {
proxy_pass http://unix:/var/lib/forgejo/socket; proxy_pass http://unix:/var/lib/forgejo/socket;
@ -19,5 +18,3 @@ server {
client_max_body_size 512M; client_max_body_size 512M;
} }
} }
{% endfor %}

View file

@ -49,7 +49,7 @@
line: '{{ item.line }}' line: '{{ item.line }}'
loop: loop:
- key: '^ALLOWED_HOSTS = ' - key: '^ALLOWED_HOSTS = '
line: "ALLOWED_HOSTS = [{{ fqdns | map('regex_replace', '^(.*)$', '\"\\1\"') | join(', ') }}]" line: "ALLOWED_HOSTS = ['{{ dns_name }}']"
- key: 'USER.*PostgreSQL username' - key: 'USER.*PostgreSQL username'
line: " 'USER': '{{ user }}', # PostgreSQL username" line: " 'USER': '{{ user }}', # PostgreSQL username"
# XXX unnecessary? # XXX unnecessary?

View file

@ -1,10 +1,9 @@
{% for fqdn in fqdns %}
server { server {
server_name {{ fqdn }}; server_name {{ dns_name }};
listen [::]:443 ssl ipv6only=off; listen [::]:443 ssl ipv6only=off;
ssl_certificate /etc/letsencrypt/live/{{ fqdn }}/fullchain.pem; ssl_certificate /etc/letsencrypt/live/{{ dns_name }}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/{{ fqdn }}/privkey.pem; ssl_certificate_key /etc/letsencrypt/live/{{ dns_name }}/privkey.pem;
client_max_body_size 100m; client_max_body_size 100m;
@ -19,5 +18,3 @@ server {
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $scheme;
} }
} }
{% endfor %}

View file

@ -26,9 +26,8 @@
- name: Get LE certificate - name: Get LE certificate
command: command:
cmd: certbot certonly --non-interactive --agree-tos --register-unsafely-without-email --webroot --webroot-path /srv/http -d {{ item }} cmd: certbot certonly --non-interactive --agree-tos --register-unsafely-without-email --webroot --webroot-path /srv/http -d {{ dns_name }}
creates: '/etc/letsencrypt/renewal/{{ item }}.conf' creates: '/etc/letsencrypt/renewal/{{ dns_name }}.conf'
loop: '{{ fqdns }}'
- name: Enable certbot renewal - name: Enable certbot renewal
cron: cron:

View file

@ -8,16 +8,16 @@ IN Ping(ACCEPT) -log nolog # don’t be rude
IN SSH(ACCEPT) -i mgmt # for ansible etc. IN SSH(ACCEPT) -i mgmt # for ansible etc.
IN ACCEPT -source {{ nodes | map('device_address') | flatten | selectattr('family.value', '==', 4) | map(attribute='address') | join(',') }} # my cluster IN ACCEPT -source {{ nodes | map('device_address') | flatten | selectattr('family.value', '==', 4) | map(attribute='address') | join(',') }} # my cluster
IN ACCEPT -source {{ nodes | map('device_address') | flatten | selectattr('family.value', '==', 6) | map(attribute='address') | join(',') }} # my cluster IN ACCEPT -source {{ nodes | map('device_address') | flatten | selectattr('family.value', '==', 6) | map(attribute='address') | join(',') }} # my cluster
{% for service in cluster.custom_fields.services %} {% for service in cluster_services %}
{% set prefixes = service | allowed_prefixes %} {% set prefixes = service | allowed_prefixes %}
{% set prefixes4 = prefixes | selectattr('family.value', '==', 4) | map('string') %} {% set prefixes4 = prefixes | selectattr('family.value', '==', 4) | map('string') %}
{% set prefixes6 = prefixes | selectattr('family.value', '==', 6) | map('string') %} {% set prefixes6 = prefixes | selectattr('family.value', '==', 6) | map('string') %}
{% set ports = service.ports | compact_numlist(range_delimiter=':') %} {% set ports = service.ports | compact_numlist(range_delimiter=':') %}
{% if prefixes4 %} {% if prefixes4 %}
IN ACCEPT -source {{ prefixes4 | join(',') }} -p {{ service.protocol }} -dport {{ ports }} # {{ service.name }} IN ACCEPT -source {{ prefixes4 | join(',') }} -p {{ service.protocol.value }} -dport {{ ports }} # {{ service.name }}
{% endif %} {% endif %}
{% if prefixes6 %} {% if prefixes6 %}
IN ACCEPT -source {{ prefixes6 | join(',') }} -p {{ service.protocol }} -dport {{ ports }} # {{ service.name }} IN ACCEPT -source {{ prefixes6 | join(',') }} -p {{ service.protocol.value }} -dport {{ ports }} # {{ service.name }}
{% endif %} {% endif %}
{% endfor %} {% endfor %}