test(flannel): add regression test for per-host interface default

- Add a focused test that renders the sample inventory's flannel_iface
  expression against fake ansible facts
- Assert a non-eth0 host (enp1s0, ens3) resolves its own interface and that
  the expression defaults from ansible_facts.default_ipv4.interface
- Wire it as a local pre-commit hook (default-interface-test)
This commit is contained in:
Timothy Stewart 2026-08-03 10:39:35 -05:00
parent 6d9f3453ac
commit a33550fa49
2 changed files with 98 additions and 0 deletions

View File

@ -0,0 +1,90 @@
#!/usr/bin/env python3
"""Assert the sample inventory resolves the flannel interface per host.
flannel_iface defaults to the host's default IPv4 interface rather than a
hardcoded eth0. This test extracts the flannel_iface expression from the
sample inventory and proves that a host whose primary interface is not named
eth0 (e.g. enp1s0, ens3) resolves the interface from ansible facts.
"""
from __future__ import print_function
import os
import re
import subprocess
from jinja2 import Environment, StrictUndefined
def repo_root():
return subprocess.check_output(
["git", "rev-parse", "--show-toplevel"], text=True
).strip()
def fail(message):
raise SystemExit("default-interface test failed: " + message)
class FakeAnsibleFacts(object):
"""Stand-in for the per-host ``ansible_facts`` dict."""
def __init__(self, default_iface, iface_ip):
self._default = {"interface": default_iface, "address": iface_ip}
self._ifaces = {
default_iface: {"ipv4": {"address": iface_ip}},
}
@property
def default_ipv4(self):
return self._default
def __getitem__(self, key):
return self._ifaces[key]
def read_all_yml(root):
path = os.path.join(root, "inventory", "sample", "group_vars", "all.yml")
with open(path, "r") as handle:
return handle.read()
def extract_value(content, key):
# Match a quoted value assigned to the key, e.g. flannel_iface: "...".
match = re.search(r"^%s:\s*\"(.+)\"\s*$" % re.escape(key), content, re.M)
if not match:
fail("could not find %s in the sample inventory" % key)
return match.group(1)
def resolve(env, expression, facts):
template = env.from_string(expression)
return template.render(ansible_facts=facts)
def main():
root = repo_root()
content = read_all_yml(root)
env = Environment(undefined=StrictUndefined)
flannel_expr = extract_value(content, "flannel_iface")
if "default_ipv4.interface" not in flannel_expr:
fail("flannel_iface no longer defaults from ansible facts")
# A host whose primary interface is enp1s0 (the core #621 scenario).
facts = FakeAnsibleFacts("enp1s0", "192.168.30.11")
resolved = resolve(env, flannel_expr, facts)
if resolved != "enp1s0":
fail("flannel_iface resolved to %r, expected enp1s0" % resolved)
# A different host with a different interface must resolve independently.
facts2 = FakeAnsibleFacts("ens3", "192.168.30.12")
resolved2 = resolve(env, flannel_expr, facts2)
if resolved2 != "ens3":
fail("flannel_iface resolved to %r, expected ens3" % resolved2)
print("default-interface regression test passed")
if __name__ == "__main__":
main()

View File

@ -84,3 +84,11 @@ repos:
language: system
pass_filenames: false
files: ^roles/k3s_server/tasks/metallb\.yml$|^\.github/scripts/test-metallb-remote-read\.sh$
- id: default-interface-test
name: default interface test
entry: python3 .github/scripts/test-default-interface.py
language: python
additional_dependencies:
- Jinja2>=3.1
pass_filenames: false
files: ^inventory/sample/group_vars/all\.yml$|^\.github/scripts/test-default-interface\.py$