From 469afb98140c2a33ecd0124381035423a2006e1e Mon Sep 17 00:00:00 2001 From: Jan Tuomi Date: Wed, 5 Aug 2026 13:53:07 +0300 Subject: Restructure jail ip references --- filter_plugins/filters.py | 76 ++++++++++++++++++++++ filter_plugins/ipv4.py | 76 ---------------------- inventory/prod/hosts.yml | 1 - inventory/test/hosts.yml | 1 - roles/host/tasks/poudriere.yml | 2 +- roles/jail/tasks/jail_setup.yml | 18 +---- roles/jail/templates/jail_conf.j2 | 8 +-- roles/jail/templates/usr_local_bin_jail_net.j2 | 23 ++----- roles/jails/01_ingress/templates/etc_pf.conf.j2 | 2 +- roles/jails/17_syncthing/defaults/main.yml | 12 ++-- roles/jails/20_immich/tasks/main.yml | 14 ++++ roles/jails/20_immich/templates/jail.conf.j2 | 12 ++-- .../templates/usr_local_etc_immich_server.env.j2 | 4 +- site.yml | 38 ++++++++++- 14 files changed, 153 insertions(+), 134 deletions(-) create mode 100644 filter_plugins/filters.py delete mode 100644 filter_plugins/ipv4.py diff --git a/filter_plugins/filters.py b/filter_plugins/filters.py new file mode 100644 index 0000000..d0043b6 --- /dev/null +++ b/filter_plugins/filters.py @@ -0,0 +1,76 @@ +"""Custom Jinja2 filters for IPv4 address math.""" +import struct +import socket + + +def _ip_to_int(ip): + return struct.unpack("!I", socket.inet_aton(ip))[0] + + +def _int_to_ip(n): + return socket.inet_ntoa(struct.pack("!I", n)) + + +def ipv4_network(cidr): + """Return the network address of a CIDR. '10.0.20.2/24' -> '10.0.20.0'""" + ip, prefix = cidr.split("/") + mask = (0xFFFFFFFF << (32 - int(prefix))) & 0xFFFFFFFF + return _int_to_ip(_ip_to_int(ip) & mask) + + +def ipv4_nth(cidr, n): + """Return the nth host address relative to the given IP. '192.168.2.0/16' | ipv4_nth(101) -> '192.168.2.101'""" + ip, _ = cidr.split("/") + return _int_to_ip(_ip_to_int(ip) + int(n)) + + +def ipv4_nth_cidr(cidr, n): + """Return the nth host address with the original prefix. '10.0.20.0/24' | ipv4_nth_cidr(101) -> '10.0.20.101/24'""" + _, prefix = cidr.split("/") + return ipv4_nth(cidr, n) + "/" + prefix + + +def ipv4_host(cidr): + """Return just the host part of a CIDR. '10.0.20.2/24' -> '10.0.20.2'""" + return cidr.split("/")[0] + + +def ipv4_prefixlen(cidr): + """Return just the prefix length. '10.0.20.2/24' -> '24'""" + return cidr.split("/")[1] + + +def normalize_zfs(entries): + """Expand short ZFS dataset names to full zroot/jails/volumes/ paths.""" + result = [] + for entry in entries: + e = dict(entry) + if '/' not in e.get('name', ''): + e['name'] = 'zroot/jails/volumes/' + e['name'] + result.append(e) + return result + + +def normalize_nullfs(entries): + """Expand short nullfs src names to full /usr/local/jails/volumes/ paths.""" + result = [] + for entry in entries: + e = dict(entry) + src = e.get('src', '') + if not src.startswith('/'): + e['src'] = '/usr/local/jails/volumes/' + src + result.append(e) + return result + + +class FilterModule(object): + def filters(self): + return { + "ipv4_network": ipv4_network, + "ipv4_nth": ipv4_nth, + "ipv4_nth_cidr": ipv4_nth_cidr, + "ipv4_host": ipv4_host, + "ipv4_prefixlen": ipv4_prefixlen, + "normalize_zfs": normalize_zfs, + "normalize_nullfs": normalize_nullfs, + } diff --git a/filter_plugins/ipv4.py b/filter_plugins/ipv4.py deleted file mode 100644 index d0043b6..0000000 --- a/filter_plugins/ipv4.py +++ /dev/null @@ -1,76 +0,0 @@ -"""Custom Jinja2 filters for IPv4 address math.""" -import struct -import socket - - -def _ip_to_int(ip): - return struct.unpack("!I", socket.inet_aton(ip))[0] - - -def _int_to_ip(n): - return socket.inet_ntoa(struct.pack("!I", n)) - - -def ipv4_network(cidr): - """Return the network address of a CIDR. '10.0.20.2/24' -> '10.0.20.0'""" - ip, prefix = cidr.split("/") - mask = (0xFFFFFFFF << (32 - int(prefix))) & 0xFFFFFFFF - return _int_to_ip(_ip_to_int(ip) & mask) - - -def ipv4_nth(cidr, n): - """Return the nth host address relative to the given IP. '192.168.2.0/16' | ipv4_nth(101) -> '192.168.2.101'""" - ip, _ = cidr.split("/") - return _int_to_ip(_ip_to_int(ip) + int(n)) - - -def ipv4_nth_cidr(cidr, n): - """Return the nth host address with the original prefix. '10.0.20.0/24' | ipv4_nth_cidr(101) -> '10.0.20.101/24'""" - _, prefix = cidr.split("/") - return ipv4_nth(cidr, n) + "/" + prefix - - -def ipv4_host(cidr): - """Return just the host part of a CIDR. '10.0.20.2/24' -> '10.0.20.2'""" - return cidr.split("/")[0] - - -def ipv4_prefixlen(cidr): - """Return just the prefix length. '10.0.20.2/24' -> '24'""" - return cidr.split("/")[1] - - -def normalize_zfs(entries): - """Expand short ZFS dataset names to full zroot/jails/volumes/ paths.""" - result = [] - for entry in entries: - e = dict(entry) - if '/' not in e.get('name', ''): - e['name'] = 'zroot/jails/volumes/' + e['name'] - result.append(e) - return result - - -def normalize_nullfs(entries): - """Expand short nullfs src names to full /usr/local/jails/volumes/ paths.""" - result = [] - for entry in entries: - e = dict(entry) - src = e.get('src', '') - if not src.startswith('/'): - e['src'] = '/usr/local/jails/volumes/' + src - result.append(e) - return result - - -class FilterModule(object): - def filters(self): - return { - "ipv4_network": ipv4_network, - "ipv4_nth": ipv4_nth, - "ipv4_nth_cidr": ipv4_nth_cidr, - "ipv4_host": ipv4_host, - "ipv4_prefixlen": ipv4_prefixlen, - "normalize_zfs": normalize_zfs, - "normalize_nullfs": normalize_nullfs, - } diff --git a/inventory/prod/hosts.yml b/inventory/prod/hosts.yml index ab2f80f..017166c 100644 --- a/inventory/prod/hosts.yml +++ b/inventory/prod/hosts.yml @@ -10,7 +10,6 @@ all: nic_wan: igc1 lan_ipv4_gateway: "192.168.0.1" lan_ipv4_cidr: "192.168.0.10/16" - poudriere_repo_ip: "192.168.2.21" jail_lan_cidr: "192.168.2.0/16" jail_lan_offset: 0 is_prod: true diff --git a/inventory/test/hosts.yml b/inventory/test/hosts.yml index 2624b52..9c6c73a 100644 --- a/inventory/test/hosts.yml +++ b/inventory/test/hosts.yml @@ -15,7 +15,6 @@ all: dns_nameserver: "10.0.20.1" lan_ipv4_gateway: "10.0.20.1" lan_ipv4_cidr: "10.0.20.2/24" - poudriere_repo_ip: "10.0.20.121" jail_lan_cidr: "10.0.20.0/24" jail_lan_offset: 100 ingress_wan_static: "10.0.20.3/24" diff --git a/roles/host/tasks/poudriere.yml b/roles/host/tasks/poudriere.yml index b46293f..71832dc 100644 --- a/roles/host/tasks/poudriere.yml +++ b/roles/host/tasks/poudriere.yml @@ -84,7 +84,7 @@ copy: content: | poudriere: { - url: "http://{{ poudriere_repo_ip }}/packages", + url: "http://{{ jail_ips.poudriere_repo }}/packages", enabled: no } dest: /usr/local/etc/pkg/repos/poudriere.conf diff --git a/roles/jail/tasks/jail_setup.yml b/roles/jail/tasks/jail_setup.yml index 2c8581d..aac91f7 100644 --- a/roles/jail/tasks/jail_setup.yml +++ b/roles/jail/tasks/jail_setup.yml @@ -84,21 +84,6 @@ label: "{{ item.name }}" delegate_to: "{{ jail_delegate_host }}" -- name: "Inherit mountpoint on ZFS volumes for {{ jail_name }}" - shell: | - source=$(zfs get -H -o source mountpoint {{ item.name }}) - if [ "$source" != "inherited" ] && [ "$source" != "received" ]; then - zfs unmount {{ item.name }} 2>/dev/null - zfs inherit mountpoint {{ item.name }} - echo "changed" - fi - loop: "{{ _zfs }}" - loop_control: - label: "{{ item.name }}" - register: _zfs_inherit - changed_when: "'changed' in _zfs_inherit.stdout" - delegate_to: "{{ jail_delegate_host }}" - - name: "Set ownership on ZFS volume host mountpoints for {{ jail_name }}" file: path: "/usr/local/jails/volumes/{{ item.name | basename }}" @@ -162,6 +147,7 @@ jail: name: "{{ jail_name }}" num: "{{ jail_num }}" + ip: "{{ jail_ips[jail_name] }}/{{ jail_lan_cidr | ipv4_prefixlen }}" devfs_ruleset: "{{ devfs_ruleset | default(4) }}" options: "{{ jail_conf_options | default([]) }}" exec_prestart: "{{ exec_prestart | default([]) }}" @@ -193,7 +179,7 @@ copy: content: | poudriere: { - url: "http://{{ hostvars[jail_delegate_host]['poudriere_repo_ip'] }}/packages", + url: "http://{{ jail_ips.poudriere_repo }}/packages", enabled: no, priority: 100 } diff --git a/roles/jail/templates/jail_conf.j2 b/roles/jail/templates/jail_conf.j2 index aa88049..f4243c7 100644 --- a/roles/jail/templates/jail_conf.j2 +++ b/roles/jail/templates/jail_conf.j2 @@ -16,10 +16,10 @@ exec.stop = "/bin/sh /etc/rc.shutdown"; # Networking - exec.prestart += "jail_net prestart {{ jail.num }} ${name}"; - exec.poststart += "jail_net poststart {{ jail.num }} ${name}"; - exec.prestop += "jail_net prestop {{ jail.num }} ${name}"; - exec.poststop += "jail_net poststop {{ jail.num }} ${name}"; + exec.prestart += "jail_net prestart ${name} {{ jail.num }} {{ jail.ip }}"; + exec.poststart += "jail_net poststart ${name} {{ jail.num }} {{ jail.ip }}"; + exec.prestop += "jail_net prestop ${name} {{ jail.num }} {{ jail.ip }}"; + exec.poststop += "jail_net poststop ${name} {{ jail.num }} {{ jail.ip }}"; # Custom hooks {% for cmd in jail.exec_prestart %} diff --git a/roles/jail/templates/usr_local_bin_jail_net.j2 b/roles/jail/templates/usr_local_bin_jail_net.j2 index e9873a3..e8ce462 100644 --- a/roles/jail/templates/usr_local_bin_jail_net.j2 +++ b/roles/jail/templates/usr_local_bin_jail_net.j2 @@ -1,27 +1,16 @@ #!/bin/sh # jail_net - manage jail LAN networking lifecycle -# Usage: jail_net +# Usage: jail_net set -e STAGE="$1" -NUM="$2" -NAME="$3" +NAME="$2" +NUM="$3" +IP="$4" BRIDGE="brlan0" -LAN_BASE="{{ jail_lan_cidr | ipv4_host }}" -LAN_PREFIX="{{ jail_lan_cidr | ipv4_prefixlen }}" -LAN_OFFSET="{{ jail_lan_offset }}" -DEFAULT_ROUTE="{{ jail_lan_cidr | ipv4_nth(1 + jail_lan_offset | int) }}" - -# Compute jail IP from jail_num + offset -_nth=$(( LAN_OFFSET + NUM )) -_a=$(echo "$LAN_BASE" | cut -d. -f1) -_b=$(echo "$LAN_BASE" | cut -d. -f2) -_c=$(echo "$LAN_BASE" | cut -d. -f3) -_d=$(echo "$LAN_BASE" | cut -d. -f4) -_total=$(( (_a << 24) + (_b << 16) + (_c << 8) + _d + _nth )) -IP="$(( (_total >> 24) & 255 )).$(( (_total >> 16) & 255 )).$(( (_total >> 8) & 255 )).$(( _total & 255 ))/${LAN_PREFIX}" +DEFAULT_ROUTE="{{ jail_ips.ingress }}" # --- Cleanup functions --- @@ -121,7 +110,7 @@ case "$STAGE" in prestop) _prestop ;; poststop) _poststop ;; *) - echo "Usage: jail_net " >&2 + echo "Usage: jail_net " >&2 exit 1 ;; esac diff --git a/roles/jails/01_ingress/templates/etc_pf.conf.j2 b/roles/jails/01_ingress/templates/etc_pf.conf.j2 index 1d8f4c8..5250f79 100644 --- a/roles/jails/01_ingress/templates/etc_pf.conf.j2 +++ b/roles/jails/01_ingress/templates/etc_pf.conf.j2 @@ -4,7 +4,7 @@ lan = "epl{{ jail.num }}b" wan = "epw{{ jail.num }}b" lan_net = "{{ lan_ipv4_network }}" -gemini_host = "{{ jail_lan_cidr | ipv4_nth(5 + jail_lan_offset | int) }}" +gemini_host = "{{ jail_ips.homepage }}" table persist diff --git a/roles/jails/17_syncthing/defaults/main.yml b/roles/jails/17_syncthing/defaults/main.yml index 9e1f3f1..d7449c3 100644 --- a/roles/jails/17_syncthing/defaults/main.yml +++ b/roles/jails/17_syncthing/defaults/main.yml @@ -1,5 +1,10 @@ userland: "15.1-RELEASE" +zfs: + - name: syncthing_config + owner: "1001" + group: "1001" + nullfs: - src: storage/docs dst: /mnt/docs @@ -9,13 +14,6 @@ nullfs: dst: /mnt/jan-systems-2025-content - src: storage/projects-ableton dst: /mnt/projects-ableton - -zfs: - - name: syncthing_config - owner: "1001" - group: "1001" - -nullfs: - src: syncthing_config dst: /usr/local/etc/syncthing diff --git a/roles/jails/20_immich/tasks/main.yml b/roles/jails/20_immich/tasks/main.yml index 39decd1..cac290e 100644 --- a/roles/jails/20_immich/tasks/main.yml +++ b/roles/jails/20_immich/tasks/main.yml @@ -10,6 +10,20 @@ owner: root group: wheel mode: "0644" + vars: + jail: + name: "{{ jail_name }}" + num: "{{ jail_num }}" + ip: "{{ jail_ips[jail_name] }}/{{ jail_lan_cidr | ipv4_prefixlen }}" + devfs_ruleset: "{{ devfs_ruleset | default(4) }}" + options: "{{ jail_conf_options | default([]) }}" + exec_prestart: "{{ exec_prestart | default([]) }}" + exec_start: "{{ exec_start | default([]) }}" + exec_poststart: "{{ exec_poststart | default([]) }}" + exec_prestop: "{{ exec_prestop | default([]) }}" + exec_stop: "{{ exec_stop | default([]) }}" + exec_poststop: "{{ exec_poststop | default([]) }}" + nullfs: "{{ nullfs | default([]) | normalize_nullfs }}" delegate_to: "{{ jail_delegate_host }}" # Deploy extraction script diff --git a/roles/jails/20_immich/templates/jail.conf.j2 b/roles/jails/20_immich/templates/jail.conf.j2 index 8e3c626..fb7f363 100644 --- a/roles/jails/20_immich/templates/jail.conf.j2 +++ b/roles/jails/20_immich/templates/jail.conf.j2 @@ -1,4 +1,4 @@ -immich { +{{ jail.name }} { vnet; persist; exec.clean; @@ -20,13 +20,13 @@ immich { exec.stop = "/bin/sh /etc/rc.shutdown"; # Networking - exec.prestart += "jail_net prestart {{ jail_num }} ${name}"; - exec.poststart += "jail_net poststart {{ jail_num }} ${name}"; - exec.prestop += "jail_net prestop {{ jail_num }} ${name}"; - exec.poststop += "jail_net poststop {{ jail_num }} ${name}"; + exec.prestart += "jail_net prestart ${name} {{ jail.num }} {{ jail.ip }}"; + exec.poststart += "jail_net poststart ${name} {{ jail.num }} {{ jail.ip }}"; + exec.prestop += "jail_net prestop ${name} {{ jail.num }} {{ jail.ip }}"; + exec.poststop += "jail_net poststop ${name} {{ jail.num }} {{ jail.ip }}"; # nullfs mounts -{% for mount in nullfs | default([]) | normalize_nullfs %} +{% for mount in jail.nullfs %} mount += "{{ mount.src }} ${path}{{ mount.dst }} nullfs {{ mount.mode | default('rw') }} 0 0"; {% endfor %} {% for img in oci_images %} diff --git a/roles/jails/20_immich/templates/usr_local_etc_immich_server.env.j2 b/roles/jails/20_immich/templates/usr_local_etc_immich_server.env.j2 index 87c563d..5712234 100644 --- a/roles/jails/20_immich/templates/usr_local_etc_immich_server.env.j2 +++ b/roles/jails/20_immich/templates/usr_local_etc_immich_server.env.j2 @@ -5,14 +5,14 @@ IMMICH_PORT=80 NODE_ENV=production # Database -DB_HOSTNAME={{ jail_lan_cidr | ipv4_nth(2 + jail_lan_offset | int) }} +DB_HOSTNAME={{ jail_ips.postgres }} DB_PORT=5432 DB_USERNAME=immich DB_PASSWORD={{ pg_user_passwords.immich }} DB_DATABASE_NAME=immich # Redis -REDIS_HOSTNAME={{ jail_lan_cidr | ipv4_nth(9 + jail_lan_offset | int) }} +REDIS_HOSTNAME={{ jail_ips.redis }} REDIS_PORT=6379 REDIS_PASSWORD={{ valkey_password }} diff --git a/site.yml b/site.yml index f55eef1..e18d5b7 100644 --- a/site.yml +++ b/site.yml @@ -15,7 +15,7 @@ vars: lan_ipv4_network: 192.168.0.0/16 lan_search_domain: local.jan.systems - ingress_ip: "{{ jail_lan_cidr | ipv4_nth(1 + jail_lan_offset | int) }}" + ingress_ip: "{{ jail_ips.ingress }}" pre_tasks: - name: Verify secrets are loaded assert: @@ -23,6 +23,33 @@ fail_msg: "Required variables missing. Did you forget to create secrets.yml?" no_log: true + - name: Discover jail directories + find: + paths: "{{ playbook_dir }}/roles/jails" + patterns: "main.yml" + file_type: file + recurse: true + delegate_to: localhost + register: _jail_specs + + - name: Build jails list + set_fact: + jails: "{{ jails | default([]) + [{'num': _num, 'name': _name}] }}" + vars: + _num: "{{ item.path | regex_replace('.*/jails/([^/]+)/.*', '\\1') | split('_') | first | int }}" + _name: "{{ item.path | regex_replace('.*/jails/([^/]+)/.*', '\\1') | regex_replace('^[0-9]+_', '') }}" + loop: "{{ _jail_specs.files | sort(attribute='path') }}" + loop_control: + label: "{{ item.path | regex_replace('.*/jails/([^/]+)/.*', '\\1') }}" + when: "'/defaults/' in item.path" + + - name: Build jail_ips dict + set_fact: + jail_ips: "{{ jail_ips | default({}) | combine({item.name: jail_lan_cidr | ipv4_nth(item.num | int + jail_lan_offset | int)}) }}" + loop: "{{ jails }}" + loop_control: + label: "{{ item.name }}" + tasks: - name: Apply host roles include_role: @@ -38,7 +65,7 @@ vars: lan_ipv4_network: 192.168.0.0/16 lan_search_domain: local.jan.systems - ingress_ip: "{{ jail_lan_cidr | ipv4_nth(1 + jail_lan_offset | int) }}" + ingress_ip: "{{ jail_ips.ingress }}" pre_tasks: - name: Resolve jail role directory set_fact: @@ -68,6 +95,13 @@ label: "{{ item.path | regex_replace('.*/jails/([^/]+)/.*', '\\1') }}" when: "'/defaults/' in item.path" + - name: Build jail_ips dict + set_fact: + jail_ips: "{{ jail_ips | default({}) | combine({item.name: jail_lan_cidr | ipv4_nth(item.num | int + jail_lan_offset | int)}) }}" + loop: "{{ jails }}" + loop_control: + label: "{{ item.name }}" + tasks: - name: Apply jail role include_role: -- cgit v1.3