Skip to content

fix: avoid crash when host has no default_ipv6 fact - #412

Open
freedomwarrior wants to merge 1 commit into
lablabs:mainfrom
freedomwarrior:fix/default-ipv6-undefined-attribute
Open

fix: avoid crash when host has no default_ipv6 fact#412
freedomwarrior wants to merge 1 commit into
lablabs:mainfrom
freedomwarrior:fix/default-ipv6-undefined-attribute

Conversation

@freedomwarrior

@freedomwarrior freedomwarrior commented Aug 5, 2026

Copy link
Copy Markdown

Summary

On a host with no IPv6 default route, ansible_facts has no default_ipv6 key at all. Several places in the role fall back to it like this:

ansible_facts.default_ipv4.address | default(ansible_facts.default_ipv6.address)

default() is just a normal Jinja filter — it evaluates both its input and its fallback argument before deciding which to use, it does not short-circuit. So even when default_ipv4 resolves fine, the default_ipv6 branch is evaluated anyway. Accessing the missing default_ipv6 key returns Jinja's Undefined placeholder, and the immediately-chained .address/.interface/['address'] on that placeholder raises:

'dict object' has no attribute 'default_ipv6'

This hits rke2_api_ip's default in argument_specs.yml during role argument validation, before the role even runs a single task, on any IPv4-only host.

  • Introduced in Ipv6 improvements #332 (ipv6-improvements), which added the default_ipv4 | default(default_ipv6) fallback pattern assuming default() short-circuits like ||.
  • Carried forward by a later rename commit that switched ansible_default_ipv4ansible_facts['default_ipv4'] but kept the same fallback shape.

Fix

Replace the dict['key']['subkey'] / dict.key.subkey chains with dict.get('key', {}).get('subkey'), which returns None instead of raising when the outer key is absent, and use or instead of default() so a None/empty primary value still falls through to the IPv6 branch.

Fixed in:

  • meta/argument_specs.yml (rke2_api_ip default — the one that breaks argument validation)
  • defaults/main.yml (same pattern, legacy ansible_default_ipv4/ansible_default_ipv6 names)
  • templates/keepalived.conf.j2 (interface, unicast_src_ip, unicast_peer)
  • templates/kube-vip/kube-vip.yml.j2 (vip_interface)

Test plan

  • On an IPv4-only host (no default_ipv6 fact present), confirm ansible-playbook no longer fails at "Validating arguments against arg spec 'main'" with 'dict object' has no attribute 'default_ipv6'
  • Confirm rke2_api_ip still resolves to the IPv4 address as before on IPv4-only hosts
  • Confirm IPv6 fallback still works on IPv6-only hosts (no default_ipv4 fact)
  • Confirm keepalived/kube-vip HA modes still render correctly on IPv4-only hosts

Jinja's default() filter evaluates its fallback argument eagerly, even
when the primary value already resolved. On hosts with no IPv6 default
route, ansible_facts has no default_ipv6 key at all, so chaining
['address']/.address/.interface onto the missing key raises:

  'dict object' has no attribute 'default_ipv6'

...even though default_ipv4 would have been used. This hit role
argument validation (rke2_api_ip default in argument_specs.yml) on any
IPv4-only host, before the role even started.

Replace the dict['key']/.key chains with .get(key, {}).get(subkey),
which returns None instead of raising when the key is absent, and use
`or` instead of default() so None/empty still falls through to the
IPv6 branch.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant