feat(edge-host): add tagged VLAN 40 sub-interface

Prerequisite for enabling keepalived on edge-1: VRRP needs layer-2 adjacency
with the other NTP members on the service VLAN, and the host has only untagged
VLAN 30. The switch port already permits VLAN 40.

Built to be additive and revertible, because enp1s0 is this headless host's
only path to the network:

  - The parent's 20-wired.network is not edited. The VLAN is attached through
    a 20-wired.network.d/ drop-in instead, so reverting is deleting three paths.
  - The VLAN .network sorts before 20-wired.network and matches explicitly on
    Name=. 20-wired.network matches broadly on Type=ether; a VLAN link types as
    'vlan' rather than 'ether' (confirmed against networkctl output) so it
    should not match anyway, but ordering makes that belt-and-braces.
  - UseRoutes, UseDNS, UseNTP and IPv6AcceptRA are all disabled on the VLAN.
    Taking a second default route and DNS from the VLAN 40 lease would make
    return traffic asymmetric across the two VLANs; untagged VLAN 30 stays the
    only default route.

Tasks are tagged vlan40 so they can be applied without running the whole
edge-host role, which would restart the Z-Wave, open3e and alloy services.

Reconfiguring the parent link is what materialises the VLAN child -- a bare
networkctl reload leaves existing links untouched -- so the handler does that
explicitly. That re-runs DHCP on enp1s0 and is the one genuinely risky step;
worth running with console access to hand.

Not yet applied: the apply was blocked pending authorisation, so edge-1 remains
in its original network state with only 20-wired.network and 80-can0.network.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-19 21:44:10 +02:00
co-authored by Claude Opus 4.8
parent 53ebbdf0ae
commit 6928448cbe
7 changed files with 110 additions and 0 deletions
@@ -23,6 +23,10 @@ edge_loki_password: "{{ vault_edge_loki_password }}"
ntp_keepalived_enabled: false
ntp_keepalived_interface: "enp1s0.40"
# Tagged VLAN 40 sub-interface (switch port permits VLAN 40; VLAN 30 stays
# untagged/native). Prerequisite for ntp_keepalived_enabled.
edge_vlan40_enabled: true
# VRRP needs ip_nonlocal_bind but NOT ip_forward. The role's default enables
# both (correct on the PVE hypervisors); enabling forwarding here would make
# edge-1 a router between IoT VLAN 30 and service VLAN 40 once the tagged
+11
View File
@@ -105,3 +105,14 @@ edge_e3oncan_mqtt_format: "{device}_{didNumber:04d}_{didName}"
# edge_sml2mqtt_mqtt_password: "{{ vault_edge_sml2mqtt_mqtt_password }}"
# edge_open3e_mqtt_password: "{{ vault_edge_open3e_mqtt_password }}"
# edge_zwavejs_session_secret: "{{ vault_edge_zwavejs_session_secret }}"
# --- Tagged VLAN 40 sub-interface. Off by default; enabled in
# inventory/group_vars/edge_host. Exists so edge-1 can join the NTP keepalived
# VRRP group, which needs layer-2 adjacency with the other members on the
# service VLAN. The untagged VLAN 30 lease stays this host's only default
# route -- see templates/10-vlan40.network.j2.
edge_vlan40_enabled: false
edge_vlan40_parent: enp1s0
edge_vlan40_id: 40
edge_vlan40_name: enp1s0.40
edge_vlan40_parent_network: 20-wired.network
@@ -6,6 +6,15 @@
cmd: networkctl reload && networkctl reconfigure {{ edge_can_interface }}
changed_when: true
# Reconfiguring the parent is what actually materialises the VLAN child -- a
# bare reload leaves the existing link as-is (see above). This re-runs DHCP on
# enp1s0, which is this headless host's only path to the network, so it is the
# one genuinely risky step in the VLAN change.
- name: Reload networkd for VLAN
ansible.builtin.shell:
cmd: networkctl reload && networkctl reconfigure {{ edge_vlan40_parent }}
changed_when: true
# Ordered before 'Restart edge compose services': the daemon must pick up the
# new default log driver before the containers are recreated onto it.
- name: Restart docker
+39
View File
@@ -172,6 +172,45 @@
mode: "0644"
notify: Reload networkd
# --- Tagged VLAN 40. Additive by construction: the parent link keeps its own
# .network file untouched and gains the VLAN via a drop-in, so reverting is a
# matter of deleting these three paths. This host is headless and enp1s0 is its
# only route to the network, hence the caution.
- name: Create VLAN sub-interface netdev
ansible.builtin.template:
src: 10-vlan40.netdev.j2
dest: "/etc/systemd/network/10-{{ edge_vlan40_name }}.netdev"
mode: "0644"
when: edge_vlan40_enabled
notify: Reload networkd for VLAN
tags: [vlan40]
- name: Configure VLAN sub-interface addressing
ansible.builtin.template:
src: 10-vlan40.network.j2
dest: "/etc/systemd/network/10-{{ edge_vlan40_name }}.network"
mode: "0644"
when: edge_vlan40_enabled
notify: Reload networkd for VLAN
tags: [vlan40]
- name: Ensure parent network drop-in directory exists
ansible.builtin.file:
path: "/etc/systemd/network/{{ edge_vlan40_parent_network }}.d"
state: directory
mode: "0755"
when: edge_vlan40_enabled
tags: [vlan40]
- name: Attach VLAN to parent link via drop-in
ansible.builtin.template:
src: vlan40-parent-dropin.conf.j2
dest: "/etc/systemd/network/{{ edge_vlan40_parent_network }}.d/10-vlan40.conf"
mode: "0644"
when: edge_vlan40_enabled
notify: Reload networkd for VLAN
tags: [vlan40]
- name: Enable systemd-networkd
ansible.builtin.service:
name: systemd-networkd
@@ -0,0 +1,11 @@
# Managed by Ansible - Do not edit manually
# Tagged VLAN {{ edge_vlan40_id }} sub-interface on {{ edge_vlan40_parent }}.
# Needed so edge-1 can join the NTP keepalived VRRP group, which requires
# layer-2 adjacency with the other members on the service VLAN.
[NetDev]
Name={{ edge_vlan40_name }}
Kind=vlan
[VLAN]
Id={{ edge_vlan40_id }}
@@ -0,0 +1,26 @@
# Managed by Ansible - Do not edit manually
# Address configuration for the VLAN {{ edge_vlan40_id }} sub-interface.
#
# Filename sorts before 20-wired.network deliberately. networkd applies the
# first matching .network in lexicographic order, and 20-wired.network matches
# broadly on Type=ether. A VLAN link types as 'vlan' rather than 'ether' so it
# should not match anyway, but ordering plus an explicit Name= match means this
# file wins regardless.
#
# The Use*=false settings are the important part: VLAN 30 (untagged) remains
# this host's only default route and DNS source. Accepting routes, DNS or RAs
# from a second DHCP lease here would install a competing default route and
# make return traffic asymmetric across the two VLANs.
[Match]
Name={{ edge_vlan40_name }}
[Network]
DHCP=ipv4
IPv6AcceptRA=false
[DHCPv4]
UseRoutes=false
UseDNS=false
UseNTP=false
UseHostname=false
@@ -0,0 +1,10 @@
# Managed by Ansible - Do not edit manually
# Drop-in for {{ edge_vlan40_parent_network }}, attaching the tagged VLAN
# sub-interface to the parent link.
#
# Deliberately a drop-in rather than an edit of the parent .network file: that
# file carries this headless host's only route to the network, so the change is
# kept additive and revertible by deleting this directory.
[Network]
VLAN={{ edge_vlan40_name }}