Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Add Support for Firewall [Interface] Groups #84

Merged
merged 6 commits into from
Aug 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions docs/source/modules/rule_interface_group.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
.. _modules_rule_interface_group:

.. include:: ../_include/head.rst

====
Rule Interface Group
====

**STATE**: unstable

**TESTS**: `Playbook <https://github.com/ansibleguy/collection_opnsense/blob/latest/tests/rule_interface_group.yml>`_

**API Docs**: `Core - Firewall <https://docs.opnsense.org/development/api/core/firewall.html>`_

**Service Docs**: `Interface Groups <https://docs.opnsense.org/manual/firewall_groups.html>`_


Definition
**********

.. csv-table:: Definition
:header: "Parameter", "Type", "Required", "Default", "Aliases", "Comment"
:widths: 15 10 10 10 10 45

"name","string","true","\-","ifname","Name of the interface group. Only texts containing letters, digits and underscores with a maximum length of 15 characters are allowed and the name may not end with a digit."
"members","list","false","ints, interfaces","Member interfaces - you must provide the network port as shown in 'Interfaces - Assignments - Network port"
"gui_group","boolean","false","true","\-","Grouping these members in the interfaces menu section"
"sequence","int","false","0","seq","Sequence used in sorting the groups"
"description","string","false","\-","desc","Optional description"

.. include:: ../_include/param_basic.rst


Examples
********

.. code-block:: yaml

- hosts: localhost
gather_facts: no
module_defaults:
group/ansibleguy.opnsense.all:
firewall: 'opnsense.template.ansibleguy.net'
api_credential_file: '/home/guy/.secret/opn.key'

ansibleguy.opnsense.list:
target: 'rule_interface_group'

tasks:
- name: Example
ansibleguy.opnsense.rule_interface_group:
name: Internal
members: ['vtnet0', 'vtnet1']
# gui_group: true
# sequence: 0
# description: 'Optional description'

- name: Adding 1
ansibleguy.opnsense.rule_interface_group:
name: Internal
members: ['vtnet0', 'vtnet1']

- name: Listing
ansibleguy.opnsense.list:
# target: 'rule_interface_group'
register: existing_entries

- name: Printing rules
ansible.bultin.debug:
var: existing_entries.data
3 changes: 3 additions & 0 deletions meta/runtime.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ action_groups:
- ansibleguy.opnsense.rule
- ansibleguy.opnsense.rule_multi
- ansibleguy.opnsense.rule_purge
- ansibleguy.opnsense.rule_interface_group
unbound:
- ansibleguy.opnsense.unbound_general
- ansibleguy.opnsense.unbound_acl
Expand Down Expand Up @@ -178,3 +179,5 @@ plugin_routing:
redirect: ansibleguy.opnsense.gateway
gw:
redirect: ansibleguy.opnsense.gateway
rule_if_group:
redirect: ansibleguy.opnsense.rule_interface_group
57 changes: 57 additions & 0 deletions plugins/module_utils/main/rule_interface_group.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from ansible.module_utils.basic import AnsibleModule

from ansible_collections.ansibleguy.opnsense.plugins.module_utils.base.api import \
Session
from ansible_collections.ansibleguy.opnsense.plugins.module_utils.helper.main import \
validate_int_fields, is_unset
from ansible_collections.ansibleguy.opnsense.plugins.module_utils.base.cls import BaseModule


class Group(BaseModule):
FIELD_ID = 'name'
CMDS = {
'add': 'addItem',
'del': 'delItem',
'set': 'setItem',
'search': 'get',
}
API_KEY_PATH = 'group.ifgroupentry'
API_KEY_PATH_REQ = 'group'
API_MOD = 'firewall'
API_CONT = 'group'
API_CMD_REL = 'reconfigure'
FIELDS_CHANGE = ['name', 'members', 'gui_group', 'sequence', 'description']
FIELDS_ALL = [FIELD_ID]
FIELDS_ALL.extend(FIELDS_CHANGE)
FIELDS_BOOL_INVERT = ['gui_group']
FIELDS_TRANSLATE = {
'name': 'ifname',
'description': 'descr',
'gui_group': 'nogroup'
}
FIELDS_TYPING = {
'bool': ['gui_group'],
'list': ['members'],
'select': ['members'],
'int': ['sequence'],
}
INT_VALIDATIONS = {
'sequence': {'min': 0, 'max': 9999},
}
EXIST_ATTR = 'group'

def __init__(self, module: AnsibleModule, result: dict, session: Session = None):
BaseModule.__init__(self=self, m=module, r=result, s=session)
self.group = {}

def check(self) -> None:
if self.p['state'] == 'present':
if is_unset(self.p['members']):
self.m.fail_json("You need to provide a 'members' to create a rule interface group!")

validate_int_fields(module=self.m, data=self.p, field_minmax=self.INT_VALIDATIONS)

self._base_check()

def update(self) -> None:
self.b.update(enable_switch=False)
10 changes: 7 additions & 3 deletions plugins/modules/list.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@
# EXAMPLES = 'https://opnsense.ansibleguy.net/en/latest/modules/list.html'

TARGETS = [
'alias', 'rule', 'route', 'gateway', 'syslog', 'package', 'unbound_host', 'unbound_domain', 'frr_ospf_general',
'frr_ospf3_general', 'unbound_forward', 'shaper_pipe', 'shaper_queue', 'shaper_rule', 'monit_service',
'monit_test', 'monit_alert', 'wireguard_server', 'bind_domain', 'wireguard_peer', 'interface_vlan',
'alias', 'rule', 'rule_interface_group', 'route', 'gateway', 'syslog', 'package', 'unbound_host', 'unbound_domain',
'frr_ospf_general', 'frr_ospf3_general', 'unbound_forward', 'shaper_pipe', 'shaper_queue', 'shaper_rule',
'monit_service', 'monit_test', 'monit_alert', 'wireguard_server', 'bind_domain', 'wireguard_peer', 'interface_vlan',
'unbound_host_alias', 'interface_vxlan', 'frr_bfd_neighbor', 'frr_bgp_general', 'frr_bgp_neighbor',
'frr_ospf3_interface', 'frr_ospf_interface', 'bind_acl', 'frr_ospf_network', 'frr_rip', 'bind_general',
'bind_blocklist', 'bind_record', 'interface_vip', 'webproxy_general', 'webproxy_cache', 'webproxy_parent',
Expand Down Expand Up @@ -74,6 +74,10 @@ def run_module():
from ansible_collections.ansibleguy.opnsense.plugins.module_utils.main.rule import \
Rule as Target_Obj

elif target == 'rule_interface_group':
from ansible_collections.ansibleguy.opnsense.plugins.module_utils.main.rule_interface_group import \
Group as Target_Obj

elif target == 'route':
from ansible_collections.ansibleguy.opnsense.plugins.module_utils.main.route import \
Route as Target_Obj
Expand Down
77 changes: 77 additions & 0 deletions plugins/modules/rule_interface_group.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-

# Copyright: (C) 2024, AnsibleGuy <[email protected]>
# GNU General Public License v3.0+ (see https://www.gnu.org/licenses/gpl-3.0.txt)

# see: https://docs.opnsense.org/development/api/core/firewall.html

from ansible.module_utils.basic import AnsibleModule

from ansible_collections.ansibleguy.opnsense.plugins.module_utils.base.handler import \
module_dependency_error, MODULE_EXCEPTIONS

try:
from ansible_collections.ansibleguy.opnsense.plugins.module_utils.helper.wrapper import module_wrapper
from ansible_collections.ansibleguy.opnsense.plugins.module_utils.defaults.main import \
OPN_MOD_ARGS, STATE_ONLY_MOD_ARG, RELOAD_MOD_ARG
from ansible_collections.ansibleguy.opnsense.plugins.module_utils.main.rule_interface_group import Group

except MODULE_EXCEPTIONS:
module_dependency_error()


# DOCUMENTATION = 'https://opnsense.ansibleguy.net/en/latest/modules/rule_interface_group.html'
# EXAMPLES = 'https://opnsense.ansibleguy.net/en/latest/modules/rule_interface_group.html'


def run_module():
module_args = dict(
name=dict(
type='str', required=True, aliases=['ifname'],
description='Name of the interface group. Only texts containing letters, '
'digits and underscores with a maximum length of 15 characters '
'are allowed and the name may not end with a digit.',
),
members=dict(
type='list', elements='str', required=False, aliases=['ints', 'interfaces'],
description='Member interfaces - you must provide the network '
"port as shown in 'Interfaces - Assignments - Network port'"
),
gui_group=dict(
type='bool', required=False, aliases=['gui'], default=True,
description='Grouping these members in the interfaces menu section'
),
sequence=dict(
type='int', required=False, default=0, aliases=['seq'],
description='Priority sequence used in sorting the groups '
),
description=dict(type='str', required=False, aliases=['desc']),
**RELOAD_MOD_ARG,
**STATE_ONLY_MOD_ARG,
**OPN_MOD_ARGS,
)

result = dict(
changed=False,
diff={
'before': {},
'after': {},
}
)

module = AnsibleModule(
argument_spec=module_args,
supports_check_mode=True,
)

module_wrapper(Group(module=module, result=result))
module.exit_json(**result)


def main():
run_module()


if __name__ == '__main__':
main()
1 change: 1 addition & 0 deletions scripts/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ run_test 'alias_purge' 0
run_test 'rule' 1
run_test 'rule_multi' 1
run_test 'rule_purge' 0
run_test 'rule_interface_group' 1
run_test 'savepoint' 1
run_test 'cron' 1
run_test 'route' 1
Expand Down
88 changes: 88 additions & 0 deletions tests/rule_interface_group.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
---

- name: Testing Interface Groups
hosts: localhost
gather_facts: no
module_defaults:
group/ansibleguy.opnsense.all:
firewall: "{{ lookup('ansible.builtin.env', 'TEST_FIREWALL') }}"
api_credential_file: "{{ lookup('ansible.builtin.env', 'TEST_API_KEY') }}"
ssl_verify: false

ansibleguy.opnsense.list:
target: 'rule_interface_group'

tasks:
- name: Listing
ansibleguy.opnsense.list:
register: opn_pre1
failed_when: >
'data' not in opn_pre1 or
opn_pre1.data | length != 2

- name: Removing - does not exist
ansibleguy.opnsense.rule_interface_group:
name: 'ANSIBLE_TEST'
state: 'absent'
register: opn_pre2
failed_when: >
opn_pre2.failed or
opn_pre2.changed

- name: Adding 1 - failing because of invalid interface (server-side)
ansibleguy.opnsense.rule_interface_group:
name: 'ANSIBLE_TEST'
members:
- 'lan'
register: opn_fail1
failed_when: not opn_fail1.failed
when: not ansible_check_mode

- name: Adding 1 - failing because of missing members
ansibleguy.opnsense.rule_interface_group:
name: 'ANSIBLE_TEST'
register: opn_fail2
failed_when: not opn_fail2.failed

- name: Adding 1
ansibleguy.opnsense.rule_interface_group:
name: 'ANSIBLE_TEST'
members:

Check failure on line 50 in tests/rule_interface_group.yml

View workflow job for this annotation

GitHub Actions / build

50:17 [trailing-spaces] trailing spaces
- 'vtnet0'
register: opn1
failed_when: >
opn1.failed or
not opn1.changed

- name: Adding 1 - nothing changed
ansibleguy.opnsense.rule_interface_group:
name: 'ANSIBLE_TEST'
members:

Check failure on line 60 in tests/rule_interface_group.yml

View workflow job for this annotation

GitHub Actions / build

60:17 [trailing-spaces] trailing spaces
- 'vtnet0'
register: opn2
failed_when: >
opn2.failed or
opn2.changed
when: not ansible_check_mode

- name: Listing
ansibleguy.opnsense.list:
register: opn3
failed_when: >
'data' not in opn3 or
opn3.data | length != 3
when: not ansible_check_mode

- name: Cleanup
ansibleguy.opnsense.rule_interface_group:
name: 'ANSIBLE_TEST'
state: 'absent'
when: not ansible_check_mode

- name: Listing
ansibleguy.opnsense.list:
register: opn_clean1
failed_when: >
'data' not in opn_clean1 or
opn_clean1.data | length != 2
when: not ansible_check_mode
Loading