1
0
Fork 0
mirror of https://github.com/ansible-collections/hetzner.hcloud.git synced 2026-02-04 16:11:49 +00:00
hetzner.hcloud/plugins/modules/firewall.py
Jonas L. 579b34e754
fix: ensure returned resource ids are integers (#651)
##### SUMMARY

The documentation states that all our returned resource ids are
integers, this change aligns our modules with the docs.

The impact of this change should be minimal, as ids are used for
identification purposes by directly forwarding the values (no
transformation of the data is done).

##### ISSUE TYPE

- Bugfix Pull Request


##### COMPONENT NAME

All modules
2025-06-11 10:09:12 +02:00

435 lines
15 KiB
Python

#!/usr/bin/python
# Copyright: (c) 2020, Hetzner Cloud GmbH <info@hetzner-cloud.de>
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import annotations
DOCUMENTATION = """
---
module: firewall
short_description: Create and manage firewalls on the Hetzner Cloud.
description:
- Create, update and manage firewalls on the Hetzner Cloud.
author:
- Lukas Kaemmerling (@lkaemmerling)
options:
id:
description:
- The ID of the Hetzner Cloud Firewall to manage.
- Only required if no firewall O(name) is given.
type: int
name:
description:
- The Name of the Hetzner Cloud Firewall to manage.
- Only required if no firewall O(id) is given, or the firewall does not exist.
type: str
labels:
description:
- User-defined labels (key-value pairs).
type: dict
rules:
description:
- List of rules the firewall contain.
type: list
elements: dict
suboptions:
description:
description:
- User defined description of this rule.
type: str
direction:
description:
- The direction of the firewall rule.
type: str
choices: [in, out]
protocol:
description:
- The protocol of the firewall rule.
type: str
choices: [icmp, tcp, udp, esp, gre]
port:
description:
- The port or port range allowed by this rule.
- A port range can be specified by separating two ports with a dash, e.g 1024-5000.
- Only used if O(rules[].protocol=tcp) or O(rules[].protocol=udp).
type: str
source_ips:
description:
- List of CIDRs that are allowed within this rule.
- Use 0.0.0.0/0 to allow all IPv4 addresses and ::/0 to allow all IPv6 addresses.
- Only used if O(rules[].direction=in).
type: list
elements: str
default: []
destination_ips:
description:
- List of CIDRs that are allowed within this rule.
- Use 0.0.0.0/0 to allow all IPv4 addresses and ::/0 to allow all IPv6 addresses.
- Only used if O(rules[].direction=out).
type: list
elements: str
default: []
force:
description:
- Force the deletion of the Firewall when still in use.
type: bool
default: false
state:
description:
- State of the firewall.
default: present
choices: [absent, present]
type: str
extends_documentation_fragment:
- hetzner.hcloud.hcloud
"""
EXAMPLES = """
- name: Create a basic firewall
hetzner.hcloud.firewall:
name: my-firewall
state: present
- name: Create a firewall with rules
hetzner.hcloud.firewall:
name: my-firewall
rules:
- description: allow icmp from everywhere
direction: in
protocol: icmp
source_ips:
- 0.0.0.0/0
- ::/0
state: present
- name: Create a firewall with labels
hetzner.hcloud.firewall:
name: my-firewall
labels:
key: value
mylabel: 123
state: present
- name: Ensure the firewall is absent (remove if needed)
hetzner.hcloud.firewall:
name: my-firewall
state: absent
"""
RETURN = """
hcloud_firewall:
description: The firewall instance.
returned: always
type: dict
contains:
id:
description: Numeric identifier of the firewall.
returned: always
type: int
sample: 1937415
name:
description: Name of the firewall.
returned: always
type: str
sample: my-firewall
labels:
description: User-defined labels (key-value pairs).
returned: always
type: dict
rules:
description: List of rules the firewall contain.
returned: always
type: list
elements: dict
contains:
description:
description: User defined description of this rule.
type: str
returned: always
sample: allow http from anywhere
direction:
description: The direction of the firewall rule.
type: str
returned: always
sample: in
protocol:
description: The protocol of the firewall rule.
type: str
returned: always
sample: tcp
port:
description: The port or port range allowed by this rule.
type: str
returned: if RV(hcloud_firewall.rules[].protocol=tcp) or RV(hcloud_firewall.rules[].protocol=udp)
sample: "80"
source_ips:
description: List of source CIDRs that are allowed within this rule.
type: list
elements: str
returned: always
sample: ["0.0.0.0/0", "::/0"]
destination_ips:
description: List of destination CIDRs that are allowed within this rule.
type: list
elements: str
returned: always
sample: []
applied_to:
description: List of Resources the Firewall is applied to.
returned: always
type: list
elements: dict
contains:
type:
description: Type of the resource.
type: str
choices: [server, label_selector]
sample: label_selector
server:
description: ID of the server.
type: int
sample: 12345
label_selector:
description: Label selector value.
type: str
sample: env=prod
applied_to_resources:
description: List of Resources the Firewall label selector is applied to.
returned: if RV(hcloud_firewall.applied_to[].type=label_selector)
type: list
elements: dict
contains:
type:
description: Type of resource referenced.
type: str
choices: [server]
sample: server
server:
description: ID of the Server.
type: int
sample: 12345
"""
import time
from ansible.module_utils.basic import AnsibleModule
from ..module_utils.hcloud import AnsibleHCloud
from ..module_utils.vendor.hcloud import APIException, HCloudException
from ..module_utils.vendor.hcloud.firewalls import (
BoundFirewall,
FirewallResource,
FirewallRule,
)
class AnsibleHCloudFirewall(AnsibleHCloud):
represent = "hcloud_firewall"
hcloud_firewall: BoundFirewall | None = None
def _prepare_result(self):
return {
"id": self.hcloud_firewall.id,
"name": self.hcloud_firewall.name,
"rules": [self._prepare_result_rule(rule) for rule in self.hcloud_firewall.rules],
"labels": self.hcloud_firewall.labels,
"applied_to": [self._prepare_result_applied_to(resource) for resource in self.hcloud_firewall.applied_to],
}
def _prepare_result_rule(self, rule: FirewallRule):
return {
"direction": rule.direction,
"protocol": rule.protocol,
"port": rule.port,
"source_ips": rule.source_ips,
"destination_ips": rule.destination_ips,
"description": rule.description,
}
def _prepare_result_applied_to(self, resource: FirewallResource):
result = {
"type": resource.type,
"server": resource.server.id if resource.server is not None else None,
"label_selector": resource.label_selector.selector if resource.label_selector is not None else None,
}
if resource.applied_to_resources is not None:
result["applied_to_resources"] = [
{
"type": item.type,
"server": item.server.id if item.server is not None else None,
}
for item in resource.applied_to_resources
]
return result
def _get_firewall(self):
try:
if self.module.params.get("id") is not None:
self.hcloud_firewall = self.client.firewalls.get_by_id(self.module.params.get("id"))
elif self.module.params.get("name") is not None:
self.hcloud_firewall = self.client.firewalls.get_by_name(self.module.params.get("name"))
except HCloudException as exception:
self.fail_json_hcloud(exception)
def _create_firewall(self):
self.module.fail_on_missing_params(required_params=["name"])
params = {
"name": self.module.params.get("name"),
"labels": self.module.params.get("labels"),
}
rules = self.module.params.get("rules")
if rules is not None:
params["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
]
if not self.module.check_mode:
try:
self.client.firewalls.create(**params)
except HCloudException as exception:
self.fail_json_hcloud(exception, params=params)
self._mark_as_changed()
self._get_firewall()
def _update_firewall(self):
name = self.module.params.get("name")
if name is not None and self.hcloud_firewall.name != name:
self.module.fail_on_missing_params(required_params=["id"])
if not self.module.check_mode:
self.hcloud_firewall.update(name=name)
self._mark_as_changed()
labels = self.module.params.get("labels")
if labels is not None and self.hcloud_firewall.labels != labels:
if not self.module.check_mode:
self.hcloud_firewall.update(labels=labels)
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()
self._get_firewall()
def present_firewall(self):
self._get_firewall()
if self.hcloud_firewall is None:
self._create_firewall()
else:
self._update_firewall()
def delete_firewall(self):
self._get_firewall()
if self.hcloud_firewall is not None:
if not self.module.check_mode:
if self.hcloud_firewall.applied_to:
if self.module.params.get("force"):
actions = self.hcloud_firewall.remove_from_resources(self.hcloud_firewall.applied_to)
for action in actions:
action.wait_until_finished()
else:
self.module.warn(
f"Firewall {self.hcloud_firewall.name} is currently used by "
"other resources. You need to unassign the resources before "
"deleting the Firewall or use force=true."
)
retry_count = 0
while True:
try:
self.hcloud_firewall.delete()
break
except APIException as exception:
if "is still in use" in exception.message and retry_count < 10:
retry_count += 1
time.sleep(0.5 * retry_count)
continue
self.fail_json_hcloud(exception)
except HCloudException as exception:
self.fail_json_hcloud(exception)
self._mark_as_changed()
self.hcloud_firewall = None
@classmethod
def define_module(cls):
return AnsibleModule(
argument_spec=dict(
id={"type": "int"},
name={"type": "str"},
labels={"type": "dict"},
rules=dict(
type="list",
elements="dict",
options=dict(
description={"type": "str"},
direction={"type": "str", "choices": ["in", "out"]},
protocol={"type": "str", "choices": ["icmp", "udp", "tcp", "esp", "gre"]},
port={"type": "str"},
source_ips={"type": "list", "elements": "str", "default": []},
destination_ips={"type": "list", "elements": "str", "default": []},
),
required_together=[["direction", "protocol"]],
required_if=[
["protocol", "udp", ["port"]],
["protocol", "tcp", ["port"]],
],
),
force={"type": "bool", "default": False},
state={
"choices": ["absent", "present"],
"default": "present",
},
**super().base_module_arguments(),
),
required_one_of=[["id", "name"]],
required_if=[["state", "present", ["name"]]],
supports_check_mode=True,
)
def main():
module = AnsibleHCloudFirewall.define_module()
hcloud = AnsibleHCloudFirewall(module)
state = module.params.get("state")
if state == "absent":
hcloud.delete_firewall()
elif state == "present":
hcloud.present_firewall()
module.exit_json(**hcloud.get_result())
if __name__ == "__main__":
main()