From 7eb7072ec59ad11095f02cc3933a275d7d26c70e Mon Sep 17 00:00:00 2001 From: Nik Afiq Date: Mon, 24 Aug 2026 17:56:59 +0900 Subject: [PATCH] feat: add vlan50-parent role for nik-debian's tagged VLAN 50 interface 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 --- ansible/playbooks/vlan50-parent.yaml | 6 ++ ansible/roles/vlan50-parent/README.md | 41 ++++++++++ .../roles/vlan50-parent/defaults/main.yaml | 20 +++++ ansible/roles/vlan50-parent/tasks/main.yaml | 74 +++++++++++++++++++ .../templates/vlan50.interface.j2 | 13 ++++ 5 files changed, 154 insertions(+) create mode 100644 ansible/playbooks/vlan50-parent.yaml create mode 100644 ansible/roles/vlan50-parent/README.md create mode 100644 ansible/roles/vlan50-parent/defaults/main.yaml create mode 100644 ansible/roles/vlan50-parent/tasks/main.yaml create mode 100644 ansible/roles/vlan50-parent/templates/vlan50.interface.j2 diff --git a/ansible/playbooks/vlan50-parent.yaml b/ansible/playbooks/vlan50-parent.yaml new file mode 100644 index 0000000..e09f717 --- /dev/null +++ b/ansible/playbooks/vlan50-parent.yaml @@ -0,0 +1,6 @@ +--- +- name: Tagged VLAN 50 parent interface on nik-debian + hosts: debian + become: true + roles: + - vlan50-parent diff --git a/ansible/roles/vlan50-parent/README.md b/ansible/roles/vlan50-parent/README.md new file mode 100644 index 0000000..a4cb9f0 --- /dev/null +++ b/ansible/roles/vlan50-parent/README.md @@ -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. diff --git a/ansible/roles/vlan50-parent/defaults/main.yaml b/ansible/roles/vlan50-parent/defaults/main.yaml new file mode 100644 index 0000000..bd29d9b --- /dev/null +++ b/ansible/roles/vlan50-parent/defaults/main.yaml @@ -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 diff --git a/ansible/roles/vlan50-parent/tasks/main.yaml b/ansible/roles/vlan50-parent/tasks/main.yaml new file mode 100644 index 0000000..6ce4937 --- /dev/null +++ b/ansible/roles/vlan50-parent/tasks/main.yaml @@ -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. diff --git a/ansible/roles/vlan50-parent/templates/vlan50.interface.j2 b/ansible/roles/vlan50-parent/templates/vlan50.interface.j2 new file mode 100644 index 0000000..896bd54 --- /dev/null +++ b/ansible/roles/vlan50-parent/templates/vlan50.interface.j2 @@ -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 }}