From 9c78e779f8ca833779bc123de74d314884f62c2a Mon Sep 17 00:00:00 2001 From: jo Date: Tue, 12 Sep 2023 10:57:35 +0200 Subject: [PATCH] feat(firewall): add firewall resources management --- .../add-firewall-resources-management.yml | 2 + plugins/modules/hcloud_firewall.py | 183 +++++++++++++++++- .../hcloud_firewall/defaults/main/main.yml | 1 + .../targets/hcloud_firewall/tasks/cleanup.yml | 10 + .../targets/hcloud_firewall/tasks/prepare.yml | 10 + .../targets/hcloud_firewall/tasks/test.yml | 65 ++++++- 6 files changed, 264 insertions(+), 7 deletions(-) create mode 100644 changelogs/fragments/add-firewall-resources-management.yml create mode 100644 tests/integration/targets/hcloud_firewall/tasks/cleanup.yml create mode 100644 tests/integration/targets/hcloud_firewall/tasks/prepare.yml diff --git a/changelogs/fragments/add-firewall-resources-management.yml b/changelogs/fragments/add-firewall-resources-management.yml new file mode 100644 index 00000000..1956fcf6 --- /dev/null +++ b/changelogs/fragments/add-firewall-resources-management.yml @@ -0,0 +1,2 @@ +minor_changes: + - hcloud_firewall Add firewall resources management diff --git a/plugins/modules/hcloud_firewall.py b/plugins/modules/hcloud_firewall.py index 66837a4b..e00446db 100644 --- a/plugins/modules/hcloud_firewall.py +++ b/plugins/modules/hcloud_firewall.py @@ -68,6 +68,44 @@ description: - User defined description of this rule. type: str + apply_to: + description: + - Resources the Firewall should be applied to. + type: list + elements: dict + suboptions: + type: + description: + - Type of the resource. + type: str + choices: [server, label_selector] + label_selector: + description: + - Label selector value, required if I(type) is `label_selector`. + type: str + server: + description: + - ID or name of the server, required if I(type) is `server`. + type: str + remove_from: + description: + - Resources the Firewall should be removed from. + type: list + elements: dict + suboptions: + type: + description: + - Type of the resource. + type: str + choices: [server, label_selector] + label_selector: + description: + - Label selector value, required if I(type) is `label_selector`. + type: str + server: + description: + - ID or name of the server, required if I(type) is `server`. + type: str state: description: - State of the firewall. @@ -96,6 +134,22 @@ description: allow icmp in state: present +- name: Apply a firewall to resources + hetzner.hcloud.hcloud_firewall: + name: my-firewall + apply_to: + - type: label_selector + label_selector: env=prod + state: present + +- name: Remove a firewall from resources + hetzner.hcloud.hcloud_firewall: + name: my-firewall + remove_from: + - type: server + server: 12345 + state: present + - name: Create a firewall with labels hetzner.hcloud.hcloud_firewall: name: my-firewall @@ -164,19 +218,61 @@ description: User-defined labels (key-value pairs) returned: always type: dict + 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 + label_selector: + description: Label selector value. + type: str + sample: environment=production + server: + description: ID of the server. + type: int + sample: 12345 + applied_to_resources: + description: List of Resources the Firewall is applied to. + returned: always + 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 typing import List, Optional from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.common.text.converters import to_native from ..module_utils.hcloud import AnsibleHCloud from ..module_utils.vendor.hcloud import APIException, HCloudException -from ..module_utils.vendor.hcloud.firewalls.domain import FirewallRule +from ..module_utils.vendor.hcloud.firewalls import ( + BoundFirewall, + FirewallResource, + FirewallResourceLabelSelector, + FirewallRule, +) class AnsibleHCloudFirewall(AnsibleHCloud): + hcloud_firewall: Optional[BoundFirewall] + def __init__(self, module): super().__init__(module, "hcloud_firewall") self.hcloud_firewall = None @@ -187,6 +283,7 @@ def _prepare_result(self): "name": to_native(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): @@ -199,6 +296,40 @@ def _prepare_result_rule(self, rule): "description": to_native(rule.description) if rule.description is not None else None, } + def _prepare_result_applied_to(self, resource: FirewallResource): + result = { + "type": resource.type, + "server": to_native(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 _update_applied_resources_payload(self, apply_to: List[dict]) -> List[FirewallResource]: + resources = [] + for resource in apply_to: + if resource["type"] == "server": + server = self.client.servers.get_by_name(resource.get("server")) + if server is None: + server = self.client.servers.get_by_id(resource.get("server")) + + resources.append(FirewallResource(type=resource["type"], server=server)) + continue + + if resource["type"] == "label_selector": + label_selector = FirewallResourceLabelSelector(selector=resource.get("label_selector")) + resources.append(FirewallResource(type=resource["type"], label_selector=label_selector)) + continue + + return resources + def _get_firewall(self): try: if self.module.params.get("id") is not None: @@ -228,6 +359,11 @@ def _create_firewall(self): ) for rule in rules ] + + apply_to: Optional[List[dict]] = self.module.params.get("apply_to") + if apply_to is not None: + params["resources"] = self._update_applied_resources_payload(apply_to) + if not self.module.check_mode: try: self.client.firewalls.create(**params) @@ -266,6 +402,25 @@ def _update_firewall(self): ] self.hcloud_firewall.set_rules(new_rules) self._mark_as_changed() + + applied_to = [self._prepare_result_applied_to(resource) for resource in self.hcloud_firewall.applied_to] + + apply_to = self.module.params.get("apply_to") + if apply_to is not None and not all(apply_to_item in applied_to for apply_to_item in apply_to): + resources = self._update_applied_resources_payload(apply_to) + actions = self.hcloud_firewall.apply_to_resources(resources=resources) + for action in actions: + action.wait_until_finished() + self._mark_as_changed() + + remove_from = self.module.params.get("remove_from") + if remove_from is not None and any(apply_to_item in applied_to for apply_to_item in remove_from): + resources = self._update_applied_resources_payload(remove_from) + actions = self.hcloud_firewall.remove_from_resources(resources=resources) + for action in actions: + action.wait_until_finished() + self._mark_as_changed() + self._get_firewall() def present_firewall(self): @@ -315,6 +470,32 @@ def define_module(cls): required_together=[["direction", "protocol"]], ), labels={"type": "dict"}, + apply_to=dict( + type="list", + elements="dict", + options=dict( + type={"type": "str", "choices": ["label_selector", "server"]}, + label_selector={"type": "str"}, + server={"type": "str"}, + ), + required_if=[ + ["type", "label_selector", ["label_selector"]], + ["type", "server", ["server"]], + ], + ), + remove_from=dict( + type="list", + elements="dict", + options=dict( + type={"type": "str", "choices": ["label_selector", "server"]}, + label_selector={"type": "str"}, + server={"type": "str"}, + ), + required_if=[ + ["type", "label_selector", ["label_selector"]], + ["type", "server", ["server"]], + ], + ), state={ "choices": ["absent", "present"], "default": "present", diff --git a/tests/integration/targets/hcloud_firewall/defaults/main/main.yml b/tests/integration/targets/hcloud_firewall/defaults/main/main.yml index 3c62b70e..441e948e 100644 --- a/tests/integration/targets/hcloud_firewall/defaults/main/main.yml +++ b/tests/integration/targets/hcloud_firewall/defaults/main/main.yml @@ -1,4 +1,5 @@ # Copyright: (c) 2019, Hetzner Cloud GmbH # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) --- +hcloud_server_name: "{{ hcloud_ns }}" hcloud_firewall_name: "{{ hcloud_ns }}" diff --git a/tests/integration/targets/hcloud_firewall/tasks/cleanup.yml b/tests/integration/targets/hcloud_firewall/tasks/cleanup.yml new file mode 100644 index 00000000..398a81b4 --- /dev/null +++ b/tests/integration/targets/hcloud_firewall/tasks/cleanup.yml @@ -0,0 +1,10 @@ +--- +- name: Cleanup test_firewall + hetzner.hcloud.hcloud_firewall: + name: "{{ hcloud_firewall_name }}" + state: absent + +- name: Cleanup test_server + hetzner.hcloud.hcloud_server: + name: "{{ hcloud_server_name }}" + state: absent diff --git a/tests/integration/targets/hcloud_firewall/tasks/prepare.yml b/tests/integration/targets/hcloud_firewall/tasks/prepare.yml new file mode 100644 index 00000000..678464aa --- /dev/null +++ b/tests/integration/targets/hcloud_firewall/tasks/prepare.yml @@ -0,0 +1,10 @@ +--- +- name: Create test_server + hetzner.hcloud.hcloud_server: + name: "{{ hcloud_server_name }}" + server_type: cx11 + image: ubuntu-22.04 + labels: + key: value + state: stopped + register: test_server diff --git a/tests/integration/targets/hcloud_firewall/tasks/test.yml b/tests/integration/targets/hcloud_firewall/tasks/test.yml index e2c3b893..e9571dc0 100644 --- a/tests/integration/targets/hcloud_firewall/tasks/test.yml +++ b/tests/integration/targets/hcloud_firewall/tasks/test.yml @@ -1,11 +1,6 @@ # Copyright: (c) 2020, Hetzner Cloud GmbH # GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) --- -- name: setup firewall to be absent - hetzner.hcloud.hcloud_firewall: - name: "{{ hcloud_firewall_name }}" - state: absent - - name: test missing required parameters on create firewall hetzner.hcloud.hcloud_firewall: register: result @@ -36,6 +31,9 @@ - 0.0.0.0/0 - ::/0 description: "allow icmp in" + apply_to: + - type: server + server: "{{ test_server.hcloud_server.id }}" labels: key: value my-label: label @@ -49,6 +47,9 @@ - firewall.hcloud_firewall.rules | selectattr('direction','equalto','in') | list | count == 1 - firewall.hcloud_firewall.rules | selectattr('protocol','equalto','icmp') | list | count == 1 - firewall.hcloud_firewall.rules | selectattr('description', 'equalto', 'allow icmp in') | list | count == 1 + - firewall.hcloud_firewall.applied_to | list | count == 1 + - firewall.hcloud_firewall.applied_to[0].type == "server" + - firewall.hcloud_firewall.applied_to[0].server == "{{ test_server.hcloud_server.id }}" - name: test create firewall idempotence hetzner.hcloud.hcloud_firewall: @@ -139,6 +140,58 @@ that: - result is not changed +- name: test update firewall remove_from + hetzner.hcloud.hcloud_firewall: + name: "{{ hcloud_firewall_name }}" + remove_from: + - type: server + server: "{{ test_server.hcloud_server.id }}" + register: result +- name: verify update firewall remove_from + assert: + that: + - result is changed + - result.hcloud_firewall.applied_to | list | count == 0 + +- name: test update firewall remove_from idempotence + hetzner.hcloud.hcloud_firewall: + name: "{{ hcloud_firewall_name }}" + remove_from: + - type: server + server: "{{ test_server.hcloud_server.id }}" + register: result +- name: verify update firewall remove_from idempotence + assert: + that: + - result is not changed + +- name: test update firewall apply_to + hetzner.hcloud.hcloud_firewall: + name: "{{ hcloud_firewall_name }}" + apply_to: + - type: label_selector + label_selector: key=value + register: result +- name: verify update firewall apply_to + assert: + that: + - result is changed + - result.hcloud_firewall.applied_to | list | count == 1 + - result.hcloud_firewall.applied_to[0].type == "label_selector" + - result.hcloud_firewall.applied_to[0].label_selector == "key=value" + +- name: test update firewall apply_to idempotence + hetzner.hcloud.hcloud_firewall: + name: "{{ hcloud_firewall_name }}" + apply_to: + - type: label_selector + label_selector: key=value + register: result +- name: verify update firewall apply_to + assert: + that: + - result is not changed + - name: test update firewall with check mode hetzner.hcloud.hcloud_firewall: id: "{{ firewall.hcloud_firewall.id }}" @@ -199,7 +252,7 @@ - result is changed - result.hcloud_firewall.name == "{{ hcloud_firewall_name }}" -- name: absent firewall +- name: test absent firewall hetzner.hcloud.hcloud_firewall: id: "{{ firewall.hcloud_firewall.id }}" state: absent