From 52c086d6386205eafed2801c3cea074c460bb280 Mon Sep 17 00:00:00 2001 From: Techno Tim Date: Wed, 5 Aug 2026 02:48:24 -0500 Subject: [PATCH] feat(metallb): add option to limit MetalLB layer2 announcements to specific interfaces (#696) - roles/k3s_server_post/templates/metallb.crs.j2: render spec.interfaces in the L2Advertisement when metal_lb_interfaces is a non-empty list, so MetalLB only announces on the configured interfaces. Empty list (default) keeps announcing on all interfaces, preserving existing behavior. - roles/k3s_server_post/defaults/main.yml: add metal_lb_interfaces: [] default - roles/k3s_server_post/meta/main.yml: add metal_lb_interfaces argument_spec - inventory/sample/group_vars/all.yml: document the new sample variable - .github/scripts/test-metallb-interfaces.py: regression test rendering the template for empty/single/multiple interfaces and confirming the BGP path is unaffected - .pre-commit-config.yaml: wire the new test into pre-commit Co-authored-by: Leo --- .github/scripts/test-metallb-interfaces.py | 88 +++++++++++++++++++ .pre-commit-config.yaml | 8 ++ inventory/sample/group_vars/all.yml | 6 ++ roles/k3s_server_post/defaults/main.yml | 1 + roles/k3s_server_post/meta/main.yml | 8 ++ .../k3s_server_post/templates/metallb.crs.j2 | 5 ++ 6 files changed, 116 insertions(+) create mode 100644 .github/scripts/test-metallb-interfaces.py diff --git a/.github/scripts/test-metallb-interfaces.py b/.github/scripts/test-metallb-interfaces.py new file mode 100644 index 0000000..00dcc91 --- /dev/null +++ b/.github/scripts/test-metallb-interfaces.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python3 +"""Regression test for the MetalLB L2Advertisement interfaces. + +`metal_lb_interfaces` restricts which network interfaces MetalLB announces +load balancer IPs on in layer2 mode. When the list is non-empty, the +L2Advertisement in roles/k3s_server_post/templates/metallb.crs.j2 must render +a `spec.interfaces` block; when it is empty (the default), no spec is rendered +so MetalLB announces on all interfaces. + +This renders the template and asserts both cases plus the BGP path (which must +not be affected by the L2 interfaces variable). +""" + +from __future__ import print_function + +import os +import subprocess + +from jinja2 import Environment, FileSystemLoader, StrictUndefined + + +def repo_root(): + return subprocess.check_output( + ["git", "rev-parse", "--show-toplevel"], text=True + ).strip() + + +def fail(message): + raise SystemExit("MetalLB interfaces test failed: " + message) + + +def render(env, extra_vars): + base_vars = { + "metal_lb_mode": "layer2", + "metal_lb_ip_range": "192.168.30.80-192.168.30.90", + } + base_vars.update(extra_vars) + template = env.get_template("metallb.crs.j2") + return template.render(**base_vars) + + +def main(): + root = repo_root() + template_dir = os.path.join( + root, "roles", "k3s_server_post", "templates" + ) + env = Environment( + loader=FileSystemLoader(template_dir), undefined=StrictUndefined + ) + + # Empty list (default): no spec.interfaces in the L2Advertisement. + output = render(env, {"metal_lb_interfaces": []}) + if "spec:\n interfaces:" in output: + fail("spec.interfaces rendered with an empty metal_lb_interfaces") + if "kind: L2Advertisement" not in output: + fail("L2Advertisement missing in layer2 mode") + + # Single interface. + output = render(env, {"metal_lb_interfaces": ["eth1"]}) + if "spec:\n interfaces:\n - eth1" not in output: + fail("single interface was not rendered in spec.interfaces") + + # Multiple interfaces. + output = render(env, {"metal_lb_interfaces": ["eth1", "eth2"]}) + if "spec:\n interfaces:\n - eth1\n - eth2" not in output: + fail("multiple interfaces were not rendered in spec.interfaces") + + # BGP mode must not emit an L2Advertisement spec at all. + output = render( + env, + { + "metal_lb_mode": "bgp", + "metal_lb_interfaces": ["eth1"], + "metal_lb_bgp_my_asn": "64513", + "metal_lb_bgp_peer_asn": "64512", + "metal_lb_bgp_peer_address": "192.168.30.1", + }, + ) + if "kind: L2Advertisement" in output: + fail("L2Advertisement rendered in bgp mode") + if "interfaces:" in output: + fail("interfaces rendered in bgp mode") + + print("MetalLB interfaces regression test passed") + + +if __name__ == "__main__": + main() diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0b80f6d..08ab344 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -93,6 +93,14 @@ repos: language: system pass_filenames: false files: ^roles/k3s_server/tasks/metallb\.yml$|^\.github/scripts/test-metallb-remote-read\.sh$ + - id: metallb-interfaces-test + name: MetalLB interfaces test + entry: python3 .github/scripts/test-metallb-interfaces.py + language: python + additional_dependencies: + - Jinja2>=3.1 + pass_filenames: false + files: ^roles/k3s_server_post/templates/metallb\.crs\.j2$|^\.github/scripts/test-metallb-interfaces\.py$ - id: metallb-deploy-condition-test name: MetalLB deploy condition test entry: python3 .github/scripts/test-metallb-deploy-condition.py diff --git a/inventory/sample/group_vars/all.yml b/inventory/sample/group_vars/all.yml index 26295ab..7b1562c 100644 --- a/inventory/sample/group_vars/all.yml +++ b/inventory/sample/group_vars/all.yml @@ -120,6 +120,12 @@ metal_lb_controller_tag_version: v0.16.0 # metallb ip range for load balancer metal_lb_ip_range: 192.168.30.80-192.168.30.90 +# (optional) limit MetalLB layer2 announcements to specific network interfaces. +# Leave empty (default) to announce on all interfaces. +# metal_lb_interfaces: +# - eth1 +# - eth2 + # Only enable if your nodes are proxmox LXC nodes, make sure to configure your proxmox nodes # in your hosts.ini file. # Please read https://gist.github.com/triangletodd/02f595cd4c0dc9aac5f7763ca2264185 before using this. diff --git a/roles/k3s_server_post/defaults/main.yml b/roles/k3s_server_post/defaults/main.yml index 0d64546..797d807 100644 --- a/roles/k3s_server_post/defaults/main.yml +++ b/roles/k3s_server_post/defaults/main.yml @@ -39,4 +39,5 @@ group_name_master: master metal_lb_mode: layer2 metal_lb_available_timeout: 240s metal_lb_controller_tag_version: v0.16.0 +metal_lb_interfaces: [] metal_lb_ip_range: 192.168.30.80-192.168.30.90 diff --git a/roles/k3s_server_post/meta/main.yml b/roles/k3s_server_post/meta/main.yml index 93c2cfb..ab81224 100644 --- a/roles/k3s_server_post/meta/main.yml +++ b/roles/k3s_server_post/meta/main.yml @@ -141,6 +141,14 @@ argument_specs: description: MetalLB ip range for load balancer default: 192.168.30.80-192.168.30.90 + metal_lb_interfaces: + description: >- + List of network interfaces on which MetalLB should announce the + load balancer IPs in layer2 mode. When empty (default), MetalLB + announces on all interfaces. + type: list + default: [] + metal_lb_controller_tag_version: description: Image tag for MetalLB default: v0.16.0 diff --git a/roles/k3s_server_post/templates/metallb.crs.j2 b/roles/k3s_server_post/templates/metallb.crs.j2 index 562f561..8ea5082 100644 --- a/roles/k3s_server_post/templates/metallb.crs.j2 +++ b/roles/k3s_server_post/templates/metallb.crs.j2 @@ -21,6 +21,11 @@ kind: L2Advertisement metadata: name: default namespace: metallb-system +{% if metal_lb_interfaces | default([]) | length > 0 %} +spec: + interfaces:{% for iface in metal_lb_interfaces %} + - {{ iface }}{% endfor %} +{% endif %} {% endif %} {% if metal_lb_mode == "bgp" %} ---