Skip to content

Commit

Permalink
feat(firewall): add firewall resources management
Browse files Browse the repository at this point in the history
  • Loading branch information
jooola committed Sep 12, 2023
1 parent 72f2741 commit 9c78e77
Show file tree
Hide file tree
Showing 6 changed files with 264 additions and 7 deletions.
2 changes: 2 additions & 0 deletions changelogs/fragments/add-firewall-resources-management.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
minor_changes:
- hcloud_firewall Add firewall resources management
183 changes: 182 additions & 1 deletion plugins/modules/hcloud_firewall.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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):
Expand All @@ -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:
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Copyright: (c) 2019, Hetzner Cloud GmbH <[email protected]>
# 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 }}"
10 changes: 10 additions & 0 deletions tests/integration/targets/hcloud_firewall/tasks/cleanup.yml
Original file line number Diff line number Diff line change
@@ -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
10 changes: 10 additions & 0 deletions tests/integration/targets/hcloud_firewall/tasks/prepare.yml
Original file line number Diff line number Diff line change
@@ -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
65 changes: 59 additions & 6 deletions tests/integration/targets/hcloud_firewall/tasks/test.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
# Copyright: (c) 2020, Hetzner Cloud GmbH <[email protected]>
# 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
Expand Down Expand Up @@ -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
Expand All @@ -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:
Expand Down Expand Up @@ -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 }}"
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 9c78e77

Please sign in to comment.