From 0d5e84b168f793be65aafe23f04ec1209b276ae4 Mon Sep 17 00:00:00 2001 From: Thoralf Rickert-Wendt Date: Wed, 4 Aug 2021 10:37:20 +0200 Subject: [PATCH] See #62 --- README.md | 87 +++++++++ defaults/main.yml | 54 ++++++ library/proxmox_firewall_alias.py | 190 +++++++++++++++++++ library/proxmox_firewall_group.py | 294 ++++++++++++++++++++++++++++++ library/proxmox_firewall_ipset.py | 227 +++++++++++++++++++++++ library/proxmox_firewall_rule.py | 226 +++++++++++++++++++++++ tasks/firewall.yml | 34 ++++ tasks/firewall_group.yml | 37 ++++ tasks/main.yml | 3 + 9 files changed, 1152 insertions(+) create mode 100644 library/proxmox_firewall_alias.py create mode 100644 library/proxmox_firewall_group.py create mode 100644 library/proxmox_firewall_ipset.py create mode 100644 library/proxmox_firewall_rule.py create mode 100644 tasks/firewall.yml create mode 100644 tasks/firewall_group.yml diff --git a/README.md b/README.md index a1fa9625..2c617f82 100644 --- a/README.md +++ b/README.md @@ -410,6 +410,11 @@ pve_groups: [] # List of group definitions to manage in PVE. See section on User pve_users: [] # List of user definitions to manage in PVE. See section on User Management. pve_storages: [] # List of storages to manage in PVE. See section on Storage Management. pve_datacenter_cfg: {} # Dictionary to configure the PVE datacenter.cfg config file. +pve_manage_firewall: false # manage the proxmox firewall by configuring aliases, ipsets, security groups and assignments +pve_firewall_cluster_enabled: true # enable cluster firewall on success +pve_firewall_aliases: [] # List of dict of aliases with keys name, comment and cidr, which can be either IPv4 or IPv6 +pve_firewall_ipsets: [] # List of dict of ip sets with keys name, comment and aliases, which is a list of aliases +pve_firewall_groups: [] # List of dict of a security group. It contains a list rules and which hosts should be assigned ``` To enable clustering with this role, configure the following variables appropriately: @@ -539,6 +544,88 @@ pve_acls: Refer to `library/proxmox_role.py` [link][user-module] and `library/proxmox_acl.py` [link][acl-module] for module documentation. +## Firewall management + +This feature is not fully implemented yet. + +A list of aliases. + +``` +pve_firewall_aliases: + - name: host1-ipv4 + cidr: 10.0.0.1 + comment: my_host + - name: host1-ipv6 + cidr: fd01:1::1 + comment: my_host + - name: host2-ipv4 + cidr: 10.0.0.2 + comment: my_host + - name: host2-ipv6 + cidr: fd01:1::2 + comment: my_host +``` + +A list of ip sets + +``` +pve_firewall_ipsets: + - name: "all-nodes" + comment: "all nodes" + aliases: + - host1-ipv4 + - host1-ipv6 + - host2-ipv4 + - host2-ipv6 + - name: "ipv4-nodes" + comment: "all nodes with ipv4 addresses" + aliases: + - host1-ipv4 + - host2-ipv4 + - name: "ipv6-nodes" + comment: "all nodes with ipv6 addresses" + aliases: + - host1-ipv6 + - host2-ipv6 +``` + +A list of security groups + +``` +pve_firewall_groups: + - name: "proxmox-hosts" + comment: "rules for all proxmox hosts" + assign_hosts: "{{ groups[proxmox_firewall_group] }}" + assign_cluster: true + rules: + - comment: "allow internal communication between all proxmox nodes" + source: "+all-nodes" + dest: "+all-nodes" + - action: "ACCEPT" + type: "in" + enable: true + dest: "+all-nodes" + proto: tcp + dport: "{{ pve_ssh_port }}" + comment: "allow all to access SSH" + log: "nolog" + - action: "ACCEPT" + type: "in" + enable: true + dest: "+all-nodes" + proto: tcp + dport: 8006 + comment: "allow all to access proxmox WebUI" + log: "nolog" + - action: "ACCEPT" + type: "in" + enable: true + dest: "+all-nodes" + macro: "HTTP" + comment: "allow Letsencrypt to access Proxmox Nodes" + log: "nolog" +``` + ## Storage Management You can use this role to manage storage within Proxmox VE (both in diff --git a/defaults/main.yml b/defaults/main.yml index a136d256..27fe19aa 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -44,3 +44,57 @@ pve_acls: [] pve_storages: [] pve_ssh_port: 22 pve_manage_ssh: true + +# enable to configure the database too +pve_manage_firewall: false +pve_firewall_cluster_enabled: true +pve_firewall_group: "{{ pve_group }}" + +# pve_firewall_aliases: +# - name: xyz +# cidr: 1.2.3.4 +# comment: my_host + +pve_firewall_ipsets: + - name: "network-nodes" + comment: "all nodes" + aliases: [] + - name: "proxmox-nodes" + comment: "all proxmox hosts" + group: "{{ pve_group }}" +pve_firewall_groups: + - name: "proxmox-hosts" + comment: "rules for all proxmox hosts" + assign_hosts: "{{ groups[proxmox_firewall_group] }}" + assign_cluster: true + rules: + - action: "ACCEPT" + type: "in" + enable: true + source: "+proxmox-nodes" + dest: "+proxmox-nodes" + comment: "allow internal communication between all proxmox nodes" + log: "nolog" + - action: "ACCEPT" + type: "in" + enable: true + dest: "+proxmox-nodes" + proto: tcp + dport: "{{ pve_ssh_port }}" + comment: "allow all to access SSH" + log: "nolog" + - action: "ACCEPT" + type: "in" + enable: true + dest: "+proxmox-nodes" + proto: tcp + dport: 8006 + comment: "allow all to access proxmox WebUI" + log: "nolog" + - action: "ACCEPT" + type: "in" + enable: true + dest: "+proxmox-nodes" + macro: "HTTP" + comment: "allow Letsencrypt to access Proxmox Nodes" + log: "nolog" diff --git a/library/proxmox_firewall_alias.py b/library/proxmox_firewall_alias.py new file mode 100644 index 00000000..9221950f --- /dev/null +++ b/library/proxmox_firewall_alias.py @@ -0,0 +1,190 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'status': ['stableinterface'], + 'supported_by': 'trickert76' +} + +DOCUMENTATION = ''' +--- +module: proxmox_firewall_alias + +short_description: Manages firewall aliases in Proxmox + +options: + name: + required: true + description: + - Name of the alias. + cidr: + required: true + description: + - CIDR of the alias. + state: + required: false + default: "present" + choices: [ "present", "absent" ] + description: + - Specifies whether the alias should exist or not. + comment: + required: false + description: + - Optionally sets the alias's comment in PVE. + +author: + - Thoralf Rickert-Wendt (@trickert76) +''' + +EXAMPLES = ''' +- name: Create alias for a host + proxmox_firewall_alias: + name: myhost + cidr: 127.0.0.1 +- name: Create special host + proxmox_firewall_alias: + name: gateway + cidr: fd80::1 + comment: Our gateway. +''' + +RETURN = ''' +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_text +from ansible.module_utils.pvesh import ProxmoxShellError +import ansible.module_utils.pvesh as pvesh + +class ProxmoxFirewallAlias(object): + def __init__(self, module, result): + self.module = module + self.result = result + self.name = module.params['name'] + self.cidr = module.params['cidr'] + self.state = module.params['state'] + self.comment = module.params['comment'] + + def lookup(self): + try: + return pvesh.get("cluster/firewall/aliases/{}".format(self.name)) + except ProxmoxShellError as e: + if e.status_code == 400: + return None + self.module.fail_json(msg=e.message, status_code=e.status_code, **self.result) + + def remove_alias(self): + try: + pvesh.delete("cluster/firewall/aliases/{}".format(self.name)) + return (True, None) + except ProxmoxShellError as e: + return (False, e.message) + + def create_alias(self): + new_alias = {} + new_alias['name'] = self.name + new_alias['cidr'] = self.cidr + if self.comment is not None: + new_alias['comment'] = self.comment + + try: + pvesh.create("cluster/firewall/aliases", **new_alias) + return (True, None) + except ProxmoxShellError as e: + return (False, e.message) + + def modify_alias(self): + existing_alias = self.lookup() + modified_alias = {} + modified_alias['cidr'] = self.cidr + if self.comment is not None: + modified_alias['comment'] = self.comment + + updated_fields = [] + error = None + + for key in modified_alias: + staged_value = modified_alias.get(key) + if key not in existing_alias or staged_value != existing_alias.get(key): + updated_fields.append(key) + + if self.module.check_mode: + self.module.exit_json(changed=bool(updated_fields), expected_changes=updated_fields) + + if not updated_fields: + # No changes necessary + return (updated_fields, error) + + try: + pvesh.set("cluster/firewall/aliases/{}".format(self.name), **modified_alias) + except ProxmoxShellError as e: + error = e.message + + return (updated_fields, error) + +def main(): + # Refer to https://pve.proxmox.com/pve-docs/api-viewer/index.html + module = AnsibleModule( + argument_spec = dict( + name=dict(type='str', required=True), + cidr=dict(type='str', required=True), + state=dict(default='present', choices=['present', 'absent'], type='str'), + comment=dict(default=None, type='str'), + ), + supports_check_mode=True + ) + + result = {} + pve = ProxmoxFirewallAlias(module, result) + + before_alias = pve.lookup() + + changed = False + error = None + result['name'] = pve.name + result['state'] = pve.state + + if pve.state == 'absent': + if before_alias is not None: + if module.check_mode: + module.exit_json(changed=True) + + (changed, error) = pve.remove_alias() + + if error is not None: + module.fail_json(name=pve.name, msg=error) + elif pve.state == 'present': + if not before_alias: + if module.check_mode: + module.exit_json(changed=True) + + (changed, error) = pve.create_alias() + else: + # modify alias (note: this function is check mode aware) + (updated_fields, error) = pve.modify_alias() + + if updated_fields: + changed = True + result['updated_fields'] = updated_fields + + if error is not None: + module.fail_json(name=pve.name, msg=error) + + result['changed'] = changed + + after_alias = pve.lookup() + if after_alias is not None: + result['alias'] = after_alias + + if module._diff: + if before_alias is None: + before_alias = '' + if after_alias is None: + after_alias = '' + result['diff'] = dict(before=before_alias, after=after_alias) + + module.exit_json(**result) + +if __name__ == '__main__': + main() diff --git a/library/proxmox_firewall_group.py b/library/proxmox_firewall_group.py new file mode 100644 index 00000000..a98e9c27 --- /dev/null +++ b/library/proxmox_firewall_group.py @@ -0,0 +1,294 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'status': ['stableinterface'], + 'supported_by': 'trickert76' +} + +DOCUMENTATION = ''' +--- +module: proxmox_firewall_group + +short_description: Manages firewall group of rules in Proxmox + +options: + name: + required: true + description: + - Name of the group. + state: + required: false + default: "present" + choices: [ "present", "absent" ] + description: + - Specifies whether the group should exist or not. + comment: + required: false + description: + - Optionally sets the group's comment in PVE. + rules: + required: true + description: + - a list of rules that should be applied. Every item is a dict of a rule. + +author: + - Thoralf Rickert-Wendt (@trickert76) +''' + +EXAMPLES = ''' +- name: Create group of rules for a host + proxmox_firewall_group: + name: proxmox + comment: rules for all proxmox hosts + rules: + - action: ACCEPT + type: in + pos: 0 + enable: true + source: +proxmox + dest: +proxmox + proto: tcp + dport: 22 + comment: 'ipset proxmox can talk to ipset proxmox' + log: nolog + - action: ACCEPT + type: in + pos: 1 + enable: true + source: +network + dest: +proxmox + macro: DNS + comment: 'ipset network can talk to ipset proxmox' + log: nolog +''' + +RETURN = ''' +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_text +from ansible.module_utils.pvesh import ProxmoxShellError +import ansible.module_utils.pvesh as pvesh + +class ProxmoxFirewallGroup(object): + def __init__(self, module, result): + self.module = module + self.result = result + self.name = module.params['name'] + self.state = module.params['state'] + self.comment = module.params['comment'] + self.rules = module.params['rules'] + + for pos, rule in enumerate(self.rules, start=0): + rule['pos'] = pos + + def create_rule(self, rule): + new_rule = {} + new_rule['pos'] = rule['pos'] + + if 'action' in rule: + new_rule['action'] = rule['action'] + else: + new_rule['action'] = 'ACCEPT' + if 'type' in rule: + new_rule['type'] = rule['type'] + else: + new_rule['type'] = 'in' + if 'enable' in rule and not bool(rule['enable']): + new_rule['enable'] = 0 + else: + new_rule['enable'] = 1 + if 'source' in rule: + new_rule['source'] = rule['source'] + if 'dest' in rule: + new_rule['dest'] = rule['dest'] + if 'macro' in rule: + new_rule['macro'] = rule['macro'] + if 'proto' in rule: + new_rule['proto'] = rule['proto'] + if 'dport' in rule: + new_rule['dport'] = rule['dport'] + if 'sport' in rule: + new_rule['sport'] = rule['sport'] + if 'comment' in rule: + new_rule['comment'] = rule['comment'] + if 'log' in rule: + new_rule['log'] = rule['log'] + else: + new_rule['log'] = 'nolog' + return new_rule + + def get_existing_rule(self, ruleset, pos): + for rule in ruleset: + if str(rule['pos']) == str(pos): + return rule + return None + + def lookup(self): + try: + groups = pvesh.get("cluster/firewall/groups") + for group in groups: + if group['group'] == self.name: + group['rules'] = [] + positions = pvesh.get("cluster/firewall/groups/{}".format(self.name)) + for position in positions: + rule = pvesh.get("cluster/firewall/groups/{}/{}".format(self.name,position['pos'])) + if 'digest' in rule: + del rule['digest'] + group['rules'].append(rule) + return group + return None + return pvesh.get("cluster/firewall/groups/{}".format(self.name)) + except ProxmoxShellError as e: + self.module.fail_json(msg=e.message, status_code=e.status_code, **self.result) + + def remove_group(self): + try: + pvesh.delete("cluster/firewall/groups/{}".format(self.name)) + return (True, None) + except ProxmoxShellError as e: + return (False, e.message) + + def create_group(self): + new_group = {} + new_group['group'] = self.name + if self.comment is not None: + new_group['comment'] = self.comment + + try: + pvesh.create("cluster/firewall/groups", **new_group) + if self.rules is not None: + for rule in list(reversed(self.rules)): + new_rule = self.create_rule(rule) + pvesh.create("cluster/firewall/groups/{}/".format(self.name), **new_rule) + return (True, None) + except ProxmoxShellError as e: + return (False, e.message) + + def modify_group(self): + existing_group = self.lookup() + modified_group = {} + if self.comment is not None: + modified_group['comment'] = self.comment + + updated_fields = [] + error = None + + for key in modified_group: + staged_value = modified_group.get(key) + if key not in existing_group or staged_value != existing_group.get(key): + updated_fields.append(key) + + if self.rules is not None and 'rules' in existing_group: + for rule in self.rules: + new_rule = self.create_rule(rule) + existing_rule = self.get_existing_rule(existing_group['rules'],new_rule['pos']) + + if existing_rule is None: + updated_fields.append('rules') + break + if new_rule != existing_rule: + updated_fields.append('rules') + break + + if self.module.check_mode: + self.module.exit_json(changed=bool(updated_fields), expected_changes=updated_fields) + + if not updated_fields: + # No changes necessary + return (updated_fields, error) + + try: + # no set handler defined + # pvesh.set("cluster/firewall/groups/{}".format(self.name), **modified_group) + + if self.rules is not None and 'rules' in existing_group: + for rule in self.rules: + new_rule = self.create_rule(rule) + existing_rule = self.get_existing_rule(existing_group['rules'], new_rule['pos']) + + if existing_rule is None: + pvesh.create("cluster/firewall/groups/{}/".format(self.name), **new_rule) + #else: + #pvesh.set("cluster/firewall/groups/{}/{}".format(self.name,new_rule['pos'])) + + for rule in existing_group['rules']: + new_rule = self.get_existing_rule(self.rules, rule['pos']) + + #if new_rule is None: + #pvesh.delete("cluster/firewall/groups/{}/{}".format(self.name,rule['pos'])) + except ProxmoxShellError as e: + error = e.message + + return (updated_fields, error) + +def main(): + # Refer to https://pve.proxmox.com/pve-docs/api-viewer/index.html + module = AnsibleModule( + argument_spec = dict( + name=dict(type='str', required=True), + state=dict(default='present', choices=['present', 'absent'], type='str'), + comment=dict(default=None, type='str'), + rules=dict(type='list', required=True), + assign_cluster=dict(type='bool', default=False), + assign_hosts=dict(type='list', required=False), + assign_vms=dict(type='list', required=False), + ), + supports_check_mode=True + ) + + result = {} + pve = ProxmoxFirewallGroup(module, result) + + before_group = pve.lookup() + + changed = False + error = None + result['name'] = pve.name + result['state'] = pve.state + + if pve.state == 'absent': + if before_group is not None: + if module.check_mode: + module.exit_json(changed=True) + + (changed, error) = pve.remove_group() + + if error is not None: + module.fail_json(name=pve.name, msg=error) + elif pve.state == 'present': + if not before_group: + if module.check_mode: + module.exit_json(changed=True) + + (changed, error) = pve.create_group() + else: + # modify group (note: this function is check mode aware) + (updated_fields, error) = pve.modify_group() + + if updated_fields: + changed = True + result['updated_fields'] = updated_fields + + if error is not None: + module.fail_json(name=pve.name, msg=error) + + result['changed'] = changed + + after_group = pve.lookup() + if after_group is not None: + result['group'] = after_group + + if module._diff: + if before_group is None: + before_group = '' + if after_group is None: + after_group = '' + result['diff'] = dict(before=before_group, after=after_group) + + module.exit_json(**result) + +if __name__ == '__main__': + main() diff --git a/library/proxmox_firewall_ipset.py b/library/proxmox_firewall_ipset.py new file mode 100644 index 00000000..58f7c056 --- /dev/null +++ b/library/proxmox_firewall_ipset.py @@ -0,0 +1,227 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'status': ['stableinterface'], + 'supported_by': 'trickert76' +} + +DOCUMENTATION = ''' +--- +module: proxmox_firewall_ipset + +short_description: Manages firewall ip sets in Proxmox + +options: + name: + required: true + description: + - Name of the ipset. + state: + required: false + default: "present" + choices: [ "present", "absent" ] + description: + - Specifies whether the ipset should exist or not. + entries: + required: false + type: list + description: + - Specifies a list of aliases or cidr that should be part of the . + comment: + required: false + description: + - Optionally sets the ipset's comment in PVE. + +author: + - Thoralf Rickert-Wendt (@trickert76) +''' + +EXAMPLES = ''' +- name: Create ipset for a host + proxmox_firewall_ipset: + name: mygroup + comment: Our hosts. + entries: + - 192.168.10.0/24 + - myaliashost +''' + +RETURN = ''' +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_text +from ansible.module_utils.pvesh import ProxmoxShellError +import ansible.module_utils.pvesh as pvesh + +class ProxmoxFirewallIPSet(object): + def __init__(self, module, result): + self.module = module + self.result = result + self.name = module.params['name'] + self.state = module.params['state'] + self.comment = module.params['comment'] + self.entries = module.params['entries'] + + def lookup(self): + try: + ipsets = pvesh.get("cluster/firewall/ipset") + for ipset in ipsets: + if ipset.get('name') == self.name: + ipset['entries'] = pvesh.get("cluster/firewall/ipset/{}".format(self.name)) + return ipset + return None + except ProxmoxShellError as e: + self.module.fail_json(msg=e.message, status_code=e.status_code, **self.result) + + def remove_ipset(self): + try: + pvesh.delete("cluster/firewall/ipset/{}".format(self.name)) + return (True, None) + except ProxmoxShellError as e: + return (False, e.message) + + def create_ipset(self): + new_ipset = {} + new_ipset['name'] = self.name + if self.comment is not None: + new_ipset['comment'] = self.comment + + try: + pvesh.create("cluster/firewall/ipset", **new_ipset) + if self.entries is not None: + for entry in self.entries: + new_entry = {} + new_entry['cidr'] = entry + pvesh.create("cluster/firewall/ipset/{}/".format(self.name), **new_entry) + return (True, None) + except ProxmoxShellError as e: + return (False, e.message) + + def modify_ipset(self): + existing_ipset = self.lookup() + staged_ipset = {} + if self.comment is not None: + staged_ipset['comment'] = self.comment + + updated_fields = [] + error = None + + for key in staged_ipset: + staged_value = to_text(staged_ipset[key]) if isinstance(staged_ipset[key], str) else staged_ipset[key] + if key not in existing_ipset or staged_value != existing_ipset.get(key): + updated_fields.append(key) + + if existing_ipset.get('entries') is not None: + existing_entries = existing_ipset.get('entries') + else: + existing_entries = [] + + if self.entries is not None: + for entry in self.entries: + found = False + for existing_entry in existing_entries: + if entry == existing_entry.get('cidr'): + found = True + break + if not found: + updated_fields.append('entries') + + if self.module.check_mode: + self.module.exit_json(changed=bool(updated_fields), expected_changes=updated_fields) + + if not updated_fields: + # No changes necessary + return (updated_fields, error) + + try: + # there is no setter + # pvesh.set("cluster/firewall/ipset/{}".format(self.name), **staged_ipset) + + if self.entries is not None: + for entry in self.entries: + found = False + for existing_entry in existing_entries: + if entry == existing_entry.get('cidr'): + found = True + break + if not found: + new_entry = {} + new_entry['cidr'] = entry + pvesh.create("cluster/firewall/ipset/{}/".format(self.name), **new_entry) + + for existing_entry in existing_entries: + if existing_entry.get('cidr') not in self.entries: + pvesh.delete("cluster/firewall/ipset/{}/{}".format(self.name, entry)) + except ProxmoxShellError as e: + error = e.message + + return (updated_fields, error) + +def main(): + # Refer to https://pve.proxmox.com/pve-docs/api-viewer/index.html + module = AnsibleModule( + argument_spec = dict( + name=dict(type='str', required=True), + state=dict(default='present', choices=['present', 'absent'], type='str'), + comment=dict(default=None, type='str'), + entries=dict(type='list') + ), + supports_check_mode=True + ) + + result = {} + pve = ProxmoxFirewallIPSet(module, result) + + before_ipset = pve.lookup() + + changed = False + error = None + result['name'] = pve.name + result['state'] = pve.state + + if pve.state == 'absent': + if before_ipset is not None: + if module.check_mode: + module.exit_json(changed=True) + + (changed, error) = ipset.remove_ipset() + + if error is not None: + module.fail_json(name=pve.name, msg=error) + elif pve.state == 'present': + if not before_ipset: + if module.check_mode: + module.exit_json(changed=True) + + (changed, error) = pve.create_ipset() + else: + # modify ipset (note: this function is check mode aware) + (updated_fields, error) = pve.modify_ipset() + + if updated_fields: + changed = True + result['updated_fields'] = updated_fields + + if error is not None: + module.fail_json(name=pve.name, msg=error) + + result['changed'] = changed + + after_ipset = pve.lookup() + if after_ipset is not None: + result['ipset'] = after_ipset + + if module._diff: + if before_ipset is None: + before_ipset = '' + if after_ipset is None: + after_ipset = '' + result['diff'] = dict(before=before_ipset, after=after_ipset) + + module.exit_json(**result) + +if __name__ == '__main__': + main() diff --git a/library/proxmox_firewall_rule.py b/library/proxmox_firewall_rule.py new file mode 100644 index 00000000..af1389e1 --- /dev/null +++ b/library/proxmox_firewall_rule.py @@ -0,0 +1,226 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +ANSIBLE_METADATA = { + 'metadata_version': '1.0', + 'status': ['stableinterface'], + 'supported_by': 'trickert76' +} + +DOCUMENTATION = ''' +--- +module: proxmox_firewall_rule + +short_description: Manages firewall assignments of rule of rules to hosts and vms in Proxmox + +options: + name: + required: true + description: + - Name of the group. + state: + required: false + default: "present" + choices: [ "present", "absent" ] + description: + - Specifies whether the group should exist or not. + qemu: + required: false + description: + - a vm id where this group of rules should be applied, needs node too + node: + required: false + description: + - a node name where this group of rules should be applied + cluster: + required: false + description: + - true, if this group should be applied to the cluster + +author: + - Thoralf Rickert-Wendt (@trickert76) +''' + +EXAMPLES = ''' +- name: Assign group of rules for a cluster + proxmox_firewall_rule: + name: proxmox + cluster: true +- name: Assign group of rules for a host + proxmox_firewall_rule: + name: proxmox + node: node1 +- name: Assign group of rules for a host + proxmox_firewall_rule: + name: vm + node: node1 + qemu: 100 +''' + +RETURN = ''' +''' + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_text +from ansible.module_utils.pvesh import ProxmoxShellError +import ansible.module_utils.pvesh as pvesh + +class ProxmoxFirewallRule(object): + def __init__(self, module, result): + self.module = module + self.result = result + self.name = module.params['name'] + self.state = module.params['state'] + self.cluster = module.params['cluster'] + self.node = module.params['node'] + self.qemu = module.params['qemu'] + + if bool(self.cluster): + self.type = 'cluster' + elif self.qemu is not None: + self.type = 'qemu' + elif self.node is not None: + self.type = 'node' + else: + self.module.fail_json(msg='unknown type, neither cluster nore node or qemu is defined', **self.result) + + def define_base_url(self): + if self.type == 'cluster': + url = "cluster/firewall/rules" + elif self.type == 'node': + url = "nodes/{}/firewall/rules".format(self.node) + elif self.type == "qemu": + url = "nodes/{}/qemu/{}/firewall/rules".format(self.node,self.qemu) + + return url + + def lookup(self): + try: + url = self.define_base_url() + + positions = pvesh.get(url) + for position in positions: + rule = pvesh.get("{}/{}".format(url,position['pos'])) + if rule['type'] == 'group' and rule['action'] == self.name: + return rule + return None + except ProxmoxShellError as e: + self.module.fail_json(msg=e.message, status_code=e.status_code, **self.result) + + def remove_rule(self, before_role): + try: + url = self.define_base_url() + pvesh.delete("{}/{}".format(url,before_role['pos'])) + return (True, None) + except ProxmoxShellError as e: + return (False, e.message) + + def create_rule(self): + new_rule = {} + new_rule['enable'] = 1 + new_rule['type'] = 'group' + new_rule['action'] = self.name + + try: + url = self.define_base_url() + pvesh.create(url, **new_rule) + return (True, None) + except ProxmoxShellError as e: + return (False, e.message) + + def modify_rule(self, existing_rule): + existing_rule = self.lookup() + modified_rule = {} + modified_rule['enable'] = 1 + modified_rule['type'] = 'group' + modified_rule['action'] = self.name + + updated_fields = [] + error = None + + for key in modified_rule: + staged_value = modified_rule.get(key) + if key not in existing_rule or staged_value != existing_rule.get(key): + updated_fields.append(key) + + if self.module.check_mode: + self.module.exit_json(changed=bool(updated_fields), expected_changes=updated_fields) + + if not updated_fields: + # No changes necessary + return (updated_fields, error) + + try: + pvesh.set(url.format(existing_rule['pos']), **modified_rule) + except ProxmoxShellError as e: + error = e.message + + return (updated_fields, error) + +def main(): + # Refer to https://pve.proxmox.com/pve-docs/api-viewer/index.html + module = AnsibleModule( + argument_spec = dict( + name=dict(type='str', required=True), + state=dict(default='present', choices=['present', 'absent'], type='str'), + cluster=dict(type='bool', default=False), + node=dict(type='str', required=False), + qemu=dict(type='str', required=False), + ), + supports_check_mode=True + ) + + result = {} + pve = ProxmoxFirewallRule(module, result) + + before_role = pve.lookup() + + changed = False + error = None + result['name'] = pve.name + result['state'] = pve.state + result['type'] = pve.type + + if pve.state == 'absent': + if before_role is not None: + if module.check_mode: + module.exit_json(changed=True) + + (changed, error) = pve.remove_rule(before_role) + + if error is not None: + module.fail_json(name=pve.name, msg=error) + elif pve.state == 'present': + if not before_role: + if module.check_mode: + module.exit_json(changed=True) + + (changed, error) = pve.create_rule() + else: + # modify rule (note: this function is check mode aware) + (updated_fields, error) = pve.modify_rule(before_role) + + if updated_fields: + changed = True + result['updated_fields'] = updated_fields + + if error is not None: + module.fail_json(name=pve.name, msg=error) + + result['changed'] = changed + + after_rule = pve.lookup() + if after_rule is not None: + result['rule'] = after_rule + + if module._diff: + if before_role is None: + before_role = '' + if after_rule is None: + after_rule = '' + result['diff'] = dict(before=before_role, after=after_rule) + + module.exit_json(**result) + +if __name__ == '__main__': + main() diff --git a/tasks/firewall.yml b/tasks/firewall.yml new file mode 100644 index 00000000..0d243f15 --- /dev/null +++ b/tasks/firewall.yml @@ -0,0 +1,34 @@ +--- +- name: "Create all aliases" + proxmox_firewall_alias: + name: "{{ pve_firewall_alias.name }}" + cidr: "{{ pve_firewall_alias.cidr }}" + comment: "{{ pve_firewall_alias.comment }}" + loop: "{{ pve_firewall_aliases | default([]) }}" + loop_control: + loop_var: pve_firewall_alias + +- name: "Create all IPset configurations" + proxmox_firewall_ipset: + name: "{{ pve_firewall_ipset.key }}" + entries: "{{ pve_firewall_ipset.value }}" + loop: "{{ pve_firewall_ipsets | default({}) | dict2items }}" + loop_control: + loop_var: pve_firewall_ipset + +- name: "Manage Cluster configuration" + include_tasks: firewall_group.yml + with_items: "{{ pve_firewall_groups | default([]) }}" + loop_control: + loop_var: pve_firewall_group + +- name: "Read current Cluster Firewall Status" + command: "pvesh get cluster/firewall/options --output-format json" + changed_when: false + register: _pve_firewall_cluster_status + +- name: "Enable Cluster Firewall" + command: "pvesh set cluster/firewall/options --enable=1" + when: + - "pve_firewall_cluster_enabled | bool" + - "1 == (_pve_firewall_cluster_status.stdout | from_json | json_query('enable'))" diff --git a/tasks/firewall_group.yml b/tasks/firewall_group.yml new file mode 100644 index 00000000..e00f49c1 --- /dev/null +++ b/tasks/firewall_group.yml @@ -0,0 +1,37 @@ +--- +- name: "Configure cluster security group of rule {{ pve_firewall_group.name }}" + proxmox_firewall_group: + name: "{{ pve_firewall_group.name }}" + comment: "{{ pve_firewall_group.comment | default(omit) }}" + rules: "{{ pve_firewall_group.rules }}" + +- name: "Assign security group of rules {{ pve_firewall_group.name }} to cluster" + proxmox_firewall_rule: + name: "{{ pve_firewall_group.name }}" + cluster: true + when: "pve_firewall_group.assign_cluster | bool" + +- name: "Assign security group of rules {{ pve_firewall_group.name }} to hosts" + proxmox_firewall_rule: + name: "{{ pve_firewall_group.name }}" + node: "{{ hostvars[_host].inventory_hostname_short }}" + with_items: "{{ groups[pve_firewall_group.assign_hosts] }}" + loop_control: + loop_var: _host + when: + - "pve_firewall_group.assign_hosts is defined" + - "pve_firewall_group.assign_vms is not defined" + +- name: "Assign security group of rules {{ pve_firewall_group.name }} to VMs" + proxmox_firewall_rule: + name: "{{ pve_firewall_group.name }}" + node: "{{ hostvars[hostvars[_host].pve_parent].inventory_hostname_short }}" + qemu: "{{ hostvars[_host].pve_vmid }}" + with_items: "{{ pve_firewall_group.assign_vms }}" + loop_control: + loop_var: _host + when: + - "pve_firewall_group.assign_vms is defined" + - "hostvars[_host].pve_vmid is defined" + - "hostvars[_host].pve_parent is defined" + \ No newline at end of file diff --git a/tasks/main.yml b/tasks/main.yml index 40ff59b9..5ab87130 100644 --- a/tasks/main.yml +++ b/tasks/main.yml @@ -305,3 +305,6 @@ - import_tasks: ssl_letsencrypt.yml when: "pve_ssl_letsencrypt | bool" + +- include_tasks: firewall.yml + when: "pve_manage_firewall | bool"