feat: add vlan50-parent role for nik-debian's tagged VLAN 50 interface
Some checks failed
validate / lint (push) Failing after 1s

Creates enp1s0.50 on nik-debian (no L3 address, VLAN 40/node IP/default
route untouched) for Multus to later attach macvlan/ipvlan workloads to.

Verified live: enp1s0.50 is up with no IPv4 address (only the automatic
IPv6 link-local, which is expected and harmless), default route
unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Nik Afiq 2026-08-24 17:56:59 +09:00
parent 325d3bc5c7
commit 7eb7072ec5
5 changed files with 154 additions and 0 deletions

View File

@ -0,0 +1,6 @@
---
- name: Tagged VLAN 50 parent interface on nik-debian
hosts: debian
become: true
roles:
- vlan50-parent

View File

@ -0,0 +1,41 @@
# vlan50-parent
Creates `enp1s0.50`, a tagged VLAN 50 parent interface on `nik-debian`, for
Multus macvlan/ipvlan attachments (Phase 3/4 of `~/repo/homelab/plan.md`).
Leaves the existing untagged VLAN 40 config (`enp1s0`'s own address,
default route, k3s node identity, host DNS) untouched.
## Before running
- Requires the managed-switch port for `nik-debian` already carrying VLAN
50 tagged in addition to its existing VLAN 40 untagged/PVID 40 — this is
the `home-network`-repo-owned switch/Flint handoff from plan.md Phase 1.
This role has no way to verify that from the host side; if the switch
isn't actually passing tagged VLAN 50 frames yet, the subinterface will
come up with link state but no VLAN 50 traffic will ever arrive.
- Confirm console/recovery access to `nik-debian` (physical/IPMI/other
out-of-band) before applying, same as any host networking change.
## What it does
- Installs the `vlan` package and loads/persists the `8021q` kernel
module.
- Writes `/etc/network/interfaces.d/enp1s0.50``iface ... inet manual`
with `vlan-raw-device enp1s0` and `vlan-id 50`. No IP address is ever
assigned to it.
- Brings the interface up now (`ifup`) if not already present, and
asserts afterward that it's UP, carries no IPv4/IPv6 address, and that
the host's own default route is still via `enp1s0` — never via
`enp1s0.50`.
## Rollback
```bash
sudo ifdown enp1s0.50 || true
sudo ip link delete enp1s0.50 2>/dev/null || true
sudo rm -f /etc/network/interfaces.d/enp1s0.50
sudo rm -f /etc/modules-load.d/8021q.conf
```
Leaves the `vlan` package and `8021q` module load itself in place (harmless
if unused); remove manually only if desired.

View File

@ -0,0 +1,20 @@
---
# Part of role: vlan50-parent
# Called by: ansible/playbooks/vlan50-parent.yaml
# Description: Default variables for nik-debian's tagged VLAN 50 parent
# interface. Implements Phase 3 of ~/repo/homelab/plan.md. Only apply
# this role after the home-network handoff confirms the managed-switch
# port for nik-debian is VLAN 40 untagged/PVID 40 *and* VLAN 50 tagged —
# this role does not and cannot verify switch-side config itself.
# Real parent interface on nik-debian — verified live 2026-08-23 via
# `ip -brief addr`; it is enp1s0, not "eth0".
vlan50_parent_interface: enp1s0
vlan50_id: 50
vlan50_subinterface: "{{ vlan50_parent_interface }}.{{ vlan50_id }}"
# No L3 address is assigned to the subinterface by this role (plan.md:
# "prefer an UP L2 parent with no L3 address" — Multus's macvlan/ipvlan
# CNI attaches directly to it at L2; the host itself never needs an
# address on VLAN 50).
vlan50_mtu: 1500

View File

@ -0,0 +1,74 @@
---
# Part of role: vlan50-parent
# Called by: ansible/playbooks/vlan50-parent.yaml
# Description: Creates nik-debian's persistent tagged VLAN 50 subinterface
# ({{ vlan50_subinterface }}) without touching its existing untagged
# VLAN 40 config (node IP, default route, k3s identity, host DNS). See
# this role's defaults/main.yaml for the switch-readiness precondition.
- name: Install the vlan package (ifupdown VLAN support)
ansible.builtin.apt:
name: vlan
state: present
update_cache: true
- name: Load the 8021q kernel module now
community.general.modprobe:
name: 8021q
state: present
- name: Persist the 8021q kernel module across reboots
ansible.builtin.copy:
dest: /etc/modules-load.d/8021q.conf
content: |
# Managed by Ansible (role: vlan50-parent).
8021q
mode: "0644"
owner: root
group: root
- name: Write the VLAN 50 subinterface definition
ansible.builtin.template:
src: vlan50.interface.j2
dest: "/etc/network/interfaces.d/{{ vlan50_subinterface }}"
mode: "0644"
owner: root
group: root
register: vlan50_iface_file
- name: Check whether the subinterface is already up
ansible.builtin.command: "ip link show {{ vlan50_subinterface }}"
register: vlan50_iface_check
changed_when: false
failed_when: false
- name: Bring up the VLAN 50 subinterface
ansible.builtin.command: "ifup {{ vlan50_subinterface }}"
when: vlan50_iface_check.rc != 0 or vlan50_iface_file is changed
- name: Confirm the subinterface is up with no L3 address
ansible.builtin.command: "ip -brief addr show {{ vlan50_subinterface }}"
register: vlan50_iface_state
changed_when: false
- name: Assert VLAN 50 parent is UP and carries no IPv4/IPv6 address
ansible.builtin.assert:
that:
- "'UP' in vlan50_iface_state.stdout or 'UNKNOWN' in vlan50_iface_state.stdout"
- vlan50_iface_state.stdout.split() | select('match', '^[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+/') | list | length == 0
fail_msg: "{{ vlan50_subinterface }} is either down or unexpectedly carries an IPv4 address: {{ vlan50_iface_state.stdout }}"
- name: Read current default route
ansible.builtin.command: ip -4 route show default
register: vlan50_current_default_route
changed_when: false
- name: Assert the host default route is still the untagged VLAN 40 interface
ansible.builtin.assert:
that:
- vlan50_current_default_route.stdout is search('dev ' + vlan50_parent_interface + '(\s|$)')
- not (vlan50_current_default_route.stdout is search('dev ' + vlan50_subinterface + '(\s|$)'))
fail_msg: >-
nik-debian's default route is not via {{ vlan50_parent_interface }}
({{ vlan50_current_default_route.stdout }}) — VLAN 50 must never
become the host default route.

View File

@ -0,0 +1,13 @@
# Managed by Ansible (role: vlan50-parent). Do not edit by hand.
#
# Tagged VLAN 50 parent for Multus macvlan/ipvlan attachments — see
# ~/repo/homelab/plan.md Phase 3/4. Deliberately "manual" (no inet
# config): this interface must never carry a host IP or become a route
# candidate. Untagged VLAN 40 on {{ vlan50_parent_interface }} itself
# (node IP, default route, k3s identity) is defined elsewhere in
# /etc/network/interfaces and is not touched by this file.
auto {{ vlan50_subinterface }}
iface {{ vlan50_subinterface }} inet manual
vlan-raw-device {{ vlan50_parent_interface }}
vlan-id {{ vlan50_id }}
mtu {{ vlan50_mtu }}