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 addresse

Always use the canonical address representation when checking if rules
changed.
This commit is contained in:
jo 2025-10-30 18:14:09 +01:00
parent 7ac361a9cc
commit 52e54ecc8f
No known key found for this signature in database
GPG key ID: B2FEC9B22722B984
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 ..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.firewalls import (
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):
represent = "hcloud_firewall"
@ -287,6 +296,7 @@ class AnsibleHCloudFirewall(AnsibleHCloud):
}
rules = self.module.params.get("rules")
if rules is not None:
rules = normalize_rules(rules)
params["rules"] = [
FirewallRule(
direction=rule["direction"],
@ -323,21 +333,24 @@ class AnsibleHCloudFirewall(AnsibleHCloud):
self._mark_as_changed()
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 not self.module.check_mode:
new_rules = [
FirewallRule(
direction=rule["direction"],
protocol=rule["protocol"],
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 [],
port=rule["port"],
description=rule["description"],
)
for rule in rules
]
self.hcloud_firewall.set_rules(new_rules)
self._mark_as_changed()
if rules is not None:
rules = normalize_rules(rules)
if rules != [self._prepare_result_rule(rule) for rule in self.hcloud_firewall.rules]:
if not self.module.check_mode:
new_rules = [
FirewallRule(
direction=rule["direction"],
protocol=rule["protocol"],
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 [],
port=rule["port"],
description=rule["description"],
)
for rule in rules
]
self.hcloud_firewall.set_rules(new_rules)
self._mark_as_changed()
self._get_firewall()

View file

@ -12,6 +12,14 @@
- result is failed
- '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
hetzner.hcloud.firewall:
name: "{{ hcloud_firewall_name }}"
@ -20,6 +28,11 @@
direction: in
protocol: icmp
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:
key: value
check_mode: true
@ -37,6 +50,11 @@
direction: in
protocol: icmp
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:
key: value
register: result
@ -45,11 +63,22 @@
that:
- result is changed
- 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].direction == "in"
- 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.applied_to | list | count == 0
@ -61,6 +90,11 @@
direction: in
protocol: icmp
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:
key: value
register: result