1
0
Fork 0
mirror of https://github.com/ansible-collections/hetzner.hcloud.git synced 2026-02-04 08:01:49 +00:00

fix: firewall idempotency with ipv6 addresses (#722)

##### SUMMARY

Always use the canonical address representation when checking if rules
changed.


Fixes #708
This commit is contained in:
Jonas L. 2025-10-31 14:45:06 +01:00 committed by GitHub
parent 7ac361a9cc
commit 907a7fd73c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 73 additions and 17 deletions

View file

@ -0,0 +1,2 @@
bugfixes:
- firewall - Ensure idempotency when using non canonical ipv6 representation in Firewall rules.

View file

@ -0,0 +1,7 @@
from __future__ import annotations
from ipaddress import ip_interface
def normalize_ip(value: str) -> str:
return str(ip_interface(value))

View file

@ -221,6 +221,7 @@ import time
from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import AnsibleModule
from ..module_utils.hcloud import AnsibleHCloud from ..module_utils.hcloud import AnsibleHCloud
from ..module_utils.ipaddress import normalize_ip
from ..module_utils.vendor.hcloud import APIException, HCloudException from ..module_utils.vendor.hcloud import APIException, HCloudException
from ..module_utils.vendor.hcloud.firewalls import ( from ..module_utils.vendor.hcloud.firewalls import (
BoundFirewall, BoundFirewall,
@ -229,6 +230,14 @@ from ..module_utils.vendor.hcloud.firewalls import (
) )
def normalize_rules(rules: list[dict]) -> list[dict]:
for rule in rules:
# Ensure ip addresses canonical representation
rule["source_ips"] = [normalize_ip(o) for o in rule["source_ips"]]
rule["destination_ips"] = [normalize_ip(o) for o in rule["destination_ips"]]
return rules
class AnsibleHCloudFirewall(AnsibleHCloud): class AnsibleHCloudFirewall(AnsibleHCloud):
represent = "hcloud_firewall" represent = "hcloud_firewall"
@ -287,6 +296,7 @@ class AnsibleHCloudFirewall(AnsibleHCloud):
} }
rules = self.module.params.get("rules") rules = self.module.params.get("rules")
if rules is not None: if rules is not None:
rules = normalize_rules(rules)
params["rules"] = [ params["rules"] = [
FirewallRule( FirewallRule(
direction=rule["direction"], direction=rule["direction"],
@ -323,21 +333,24 @@ class AnsibleHCloudFirewall(AnsibleHCloud):
self._mark_as_changed() self._mark_as_changed()
rules = self.module.params.get("rules") rules = self.module.params.get("rules")
if rules is not None and rules != [self._prepare_result_rule(rule) for rule in self.hcloud_firewall.rules]: if rules is not None:
if not self.module.check_mode: rules = normalize_rules(rules)
new_rules = [
FirewallRule( if rules != [self._prepare_result_rule(rule) for rule in self.hcloud_firewall.rules]:
direction=rule["direction"], if not self.module.check_mode:
protocol=rule["protocol"], new_rules = [
source_ips=rule["source_ips"] if rule["source_ips"] is not None else [], FirewallRule(
destination_ips=rule["destination_ips"] if rule["destination_ips"] is not None else [], direction=rule["direction"],
port=rule["port"], protocol=rule["protocol"],
description=rule["description"], source_ips=rule["source_ips"] if rule["source_ips"] is not None else [],
) destination_ips=rule["destination_ips"] if rule["destination_ips"] is not None else [],
for rule in rules port=rule["port"],
] description=rule["description"],
self.hcloud_firewall.set_rules(new_rules) )
self._mark_as_changed() for rule in rules
]
self.hcloud_firewall.set_rules(new_rules)
self._mark_as_changed()
self._get_firewall() self._get_firewall()

View file

@ -12,6 +12,14 @@
- result is failed - result is failed
- 'result.msg == "one of the following is required: id, name"' - 'result.msg == "one of the following is required: id, name"'
- name: Save allowed ssh sources
ansible.builtin.set_fact:
allowed_ssh_source_ips:
- "201.10.10.0/24" # ipv4 network
- "201.11.11.2/32" # ipv4 host
- "2a02:1234:10:0100::/64" # ipv6 network (non canonical representation)
- "2a02:1234:11:10::1/128" # ipv6 host
- name: Test create with check mode - name: Test create with check mode
hetzner.hcloud.firewall: hetzner.hcloud.firewall:
name: "{{ hcloud_firewall_name }}" name: "{{ hcloud_firewall_name }}"
@ -20,6 +28,11 @@
direction: in direction: in
protocol: icmp protocol: icmp
source_ips: ["0.0.0.0/0", "::/0"] source_ips: ["0.0.0.0/0", "::/0"]
- description: allow ssh in
direction: in
protocol: tcp
port: 22
source_ips: "{{ allowed_ssh_source_ips }}"
labels: labels:
key: value key: value
check_mode: true check_mode: true
@ -37,6 +50,11 @@
direction: in direction: in
protocol: icmp protocol: icmp
source_ips: ["0.0.0.0/0", "::/0"] source_ips: ["0.0.0.0/0", "::/0"]
- description: allow ssh in
direction: in
protocol: tcp
port: 22
source_ips: "{{ allowed_ssh_source_ips }}"
labels: labels:
key: value key: value
register: result register: result
@ -45,11 +63,22 @@
that: that:
- result is changed - result is changed
- result.hcloud_firewall.name == hcloud_firewall_name - result.hcloud_firewall.name == hcloud_firewall_name
- result.hcloud_firewall.rules | list | count == 1 - result.hcloud_firewall.rules | list | count == 2
- result.hcloud_firewall.rules[0].description == "allow icmp in" - result.hcloud_firewall.rules[0].description == "allow icmp in"
- result.hcloud_firewall.rules[0].direction == "in" - result.hcloud_firewall.rules[0].direction == "in"
- result.hcloud_firewall.rules[0].protocol == "icmp" - result.hcloud_firewall.rules[0].protocol == "icmp"
- result.hcloud_firewall.rules[0].source_ips == ["0.0.0.0/0", "::/0"] - result.hcloud_firewall.rules[0].source_ips | count == 2
- result.hcloud_firewall.rules[0].source_ips[0] == "0.0.0.0/0"
- result.hcloud_firewall.rules[0].source_ips[1] == "::/0"
- result.hcloud_firewall.rules[1].description == "allow ssh in"
- result.hcloud_firewall.rules[1].direction == "in"
- result.hcloud_firewall.rules[1].protocol == "tcp"
- result.hcloud_firewall.rules[1].port == "22"
- result.hcloud_firewall.rules[1].source_ips | count == 4
- result.hcloud_firewall.rules[1].source_ips[0] == "201.10.10.0/24"
- result.hcloud_firewall.rules[1].source_ips[1] == "201.11.11.2/32"
- result.hcloud_firewall.rules[1].source_ips[2] == "2a02:1234:10:100::/64" # canonical representation
- result.hcloud_firewall.rules[1].source_ips[3] == "2a02:1234:11:10::1/128"
- result.hcloud_firewall.labels.key == "value" - result.hcloud_firewall.labels.key == "value"
- result.hcloud_firewall.applied_to | list | count == 0 - result.hcloud_firewall.applied_to | list | count == 0
@ -61,6 +90,11 @@
direction: in direction: in
protocol: icmp protocol: icmp
source_ips: ["0.0.0.0/0", "::/0"] source_ips: ["0.0.0.0/0", "::/0"]
- description: allow ssh in
direction: in
protocol: tcp
port: 22
source_ips: "{{ allowed_ssh_source_ips }}"
labels: labels:
key: value key: value
register: result register: result