From 8e2d72cf77440346ef5802b9d3541e8e0cde965e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20W=C3=B6lfle?= Date: Wed, 1 Jan 2025 17:54:42 +0100 Subject: [PATCH 1/4] add module to manage Kea DHCP subnets Prior to this change, it was not possible to manage the subnets in the Kea DHCP service. I.e. it was not possible to add/remove subnets. This change adds a new module that allows to manage subnets. The code is split in two python files. 'plugins/modules/dhcp_subnet.py' and 'plugins/module_utils/main/dhcp_subnet_v4.py' --- meta/runtime.yml | 1 + plugins/module_utils/main/dhcp_subnet_v4.py | 46 ++++++++++++++++ plugins/modules/dhcp_subnet.py | 60 +++++++++++++++++++++ plugins/modules/list.py | 7 ++- 4 files changed, 113 insertions(+), 1 deletion(-) create mode 100644 plugins/module_utils/main/dhcp_subnet_v4.py create mode 100644 plugins/modules/dhcp_subnet.py diff --git a/meta/runtime.yml b/meta/runtime.yml index 42762b2..64adf4c 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -127,6 +127,7 @@ action_groups: dhcp: - ansibleguy.opnsense.dhcp_reservation - ansibleguy.opnsense.dhcp_controlagent + - ansibleguy.opnsense.dhcp_subnet acme: - ansibleguy.opnsense.acme_general - ansibleguy.opnsense.acme_account diff --git a/plugins/module_utils/main/dhcp_subnet_v4.py b/plugins/module_utils/main/dhcp_subnet_v4.py new file mode 100644 index 0000000..0df85ca --- /dev/null +++ b/plugins/module_utils/main/dhcp_subnet_v4.py @@ -0,0 +1,46 @@ +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 \ + is_ip, is_network, is_unset +from ansible_collections.ansibleguy.opnsense.plugins.module_utils.base.cls import BaseModule + +class SubnetV4(BaseModule): + FIELD_ID = 'subnet' + CMDS = { + 'add': 'addSubnet', + 'del': 'delSubnet', + 'set': 'setSubnet', + 'search': 'searchSubnet', + 'detail': 'getSubnet', + } + API_KEY_PATH = 'subnet4' + API_MOD = 'kea' + API_CONT = 'dhcpv4' + API_CONT_REL = 'service' + FIELDS_CHANGE = [ + 'subnet', 'description', 'pools' + ] + FIELDS_ALL = [FIELD_ID] + FIELDS_ALL.extend(FIELDS_CHANGE) + FIELDS_TYPING = {} + FIELDS_TRANSLATE = {} + EXIST_ATTR = 'subnet' + + def __init__(self, module: AnsibleModule, result: dict, session: Session = None): + BaseModule.__init__(self=self, m=module, r=result, s=session) + self.subnet = {} + self.existing_subnets = None + + def check(self) -> None: + if self.p['state'] == 'present': + if is_unset(self.p['subnet']): + self.m.fail_json( + "You need to provide the 'subnet' you want to create. E.g. (192.168.1.0/24)!" + ) + + if is_unset(self.p['pools']): + self.m.fail_json("You need to provide the IP 'pools' to be used in the subnet!") + + self._base_check() diff --git a/plugins/modules/dhcp_subnet.py b/plugins/modules/dhcp_subnet.py new file mode 100644 index 0000000..6df320c --- /dev/null +++ b/plugins/modules/dhcp_subnet.py @@ -0,0 +1,60 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: (C) 2024, AnsibleGuy +# GNU General Public License v3.0+ (see https://www.gnu.org/licenses/gpl-3.0.txt) + +# see: https://docs.opnsense.org/development/api/core/kea.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_MOD_ARG, RELOAD_MOD_ARG + from ansible_collections.ansibleguy.opnsense.plugins.module_utils.main.dhcp_subnet_v4 import SubnetV4 + +except MODULE_EXCEPTIONS: + module_dependency_error() + + +# DOCUMENTATION = 'https://opnsense.ansibleguy.net/modules/dhcp.html' +# EXAMPLES = 'https://opnsense.ansibleguy.net/modules/dhcp.html' + + +def run_module(): + module_args = dict( + subnet=dict(type='str', required=True, description='Subnet to create'), + description=dict(type='str', required=False), + pools=dict(type='str', required=True, description='IP address pools to offer in the subnet'), + **RELOAD_MOD_ARG, + **STATE_MOD_ARG, + **OPN_MOD_ARGS, + ) + + result = dict( + changed=False, + diff={ + 'before': {}, + 'after': {}, + } + ) + + module = AnsibleModule( + argument_spec=module_args, + supports_check_mode=True, + ) + + module_wrapper(SubnetV4(module=module, result=result)) + module.exit_json(**result) + + +def main(): + run_module() + + +if __name__ == '__main__': + main() diff --git a/plugins/modules/list.py b/plugins/modules/list.py index 1aa02c5..92e7d17 100644 --- a/plugins/modules/list.py +++ b/plugins/modules/list.py @@ -36,7 +36,7 @@ 'ipsec_child', 'ipsec_vti', 'ipsec_auth_local', 'ipsec_auth_remote', 'frr_general', 'unbound_general', 'unbound_acl', 'ids_general', 'ids_policy', 'ids_rule', 'ids_ruleset', 'ids_user_rule', 'ids_policy_rule', 'openvpn_instance', 'openvpn_static_key', 'openvpn_client_override', 'dhcrelay_destination', 'dhcrelay_relay', - 'interface_lagg', 'interface_loopback', 'unbound_dnsbl', 'dhcp_reservation', 'acme_general', 'acme_account', + 'interface_lagg', 'interface_loopback', 'unbound_dnsbl', 'dhcp_reservation', 'dhcp_subnet', 'acme_general', 'acme_account', 'acme_validation', 'acme_action', 'acme_certificate', ] @@ -419,6 +419,11 @@ def run_module(): from ansible_collections.ansibleguy.opnsense.plugins.module_utils.main.acme_certificate import \ Certificate as Target_Obj + elif target == 'dhcp_subnet': + from ansible_collections.ansibleguy.opnsense.plugins.module_utils.main.dhcp_subnet_v4 import \ + SubnetV4 as Target_Obj + + except AttributeError: module_dependency_error() From 4c346d3ae39388d3b5501ae5c5fc6e4011918154 Mon Sep 17 00:00:00 2001 From: Rath Pascal Date: Sat, 11 Jan 2025 19:35:24 +0100 Subject: [PATCH 2/4] implement dhcp_subnet module --- plugins/module_utils/helper/main.py | 3 + plugins/module_utils/main/dhcp_subnet_v4.py | 91 +++++++++++--- plugins/modules/dhcp_subnet.py | 73 ++++++++++- plugins/modules/list.py | 1 - scripts/test.sh | 1 + tests/1_cleanup.yml | 9 ++ tests/dhcp_reservation.yml | 13 +- tests/dhcp_subnet.yml | 133 ++++++++++++++++++++ 8 files changed, 302 insertions(+), 22 deletions(-) create mode 100644 tests/dhcp_subnet.yml diff --git a/plugins/module_utils/helper/main.py b/plugins/module_utils/helper/main.py index 01f69a6..4ab3f41 100644 --- a/plugins/module_utils/helper/main.py +++ b/plugins/module_utils/helper/main.py @@ -446,6 +446,9 @@ def simplify_translate( # correct value types to match (for diff-checks) for t, fields in typing.items(): for f in fields: + if f in ignore: + continue + if t == 'bool': simple[f] = is_true(simple[f]) diff --git a/plugins/module_utils/main/dhcp_subnet_v4.py b/plugins/module_utils/main/dhcp_subnet_v4.py index 0df85ca..310c74b 100644 --- a/plugins/module_utils/main/dhcp_subnet_v4.py +++ b/plugins/module_utils/main/dhcp_subnet_v4.py @@ -3,11 +3,11 @@ from ansible_collections.ansibleguy.opnsense.plugins.module_utils.base.api import \ Session from ansible_collections.ansibleguy.opnsense.plugins.module_utils.helper.main import \ - is_ip, is_network, is_unset + get_selected_list, simplify_translate from ansible_collections.ansibleguy.opnsense.plugins.module_utils.base.cls import BaseModule + class SubnetV4(BaseModule): - FIELD_ID = 'subnet' CMDS = { 'add': 'addSubnet', 'del': 'delSubnet', @@ -15,17 +15,36 @@ class SubnetV4(BaseModule): 'search': 'searchSubnet', 'detail': 'getSubnet', } + API_KEY = 'subnet4' API_KEY_PATH = 'subnet4' API_MOD = 'kea' API_CONT = 'dhcpv4' API_CONT_REL = 'service' FIELDS_CHANGE = [ - 'subnet', 'description', 'pools' + 'subnet', 'description', 'pools', 'auto_options', + ] + FIELDS_ALL = FIELDS_CHANGE + FIELDS_TYPING = { + 'list': ['gateway', 'dns', 'domain_search', 'ntp_servers', 'time_servers'], # 'pools', + 'bool': ['auto_options'], + } + FIELDS_TRANSLATE = { + 'auto_options': 'option_data_autocollect', + } + API_ATTR_OPTIONS = 'option_data' + API_FIELDS_OPTIONS = [ + 'gateway', 'routes', 'dns', 'domain', 'domain_search', 'ntp_servers', 'time_servers', + 'next_server', 'tftp_server', 'tftp_file', ] - FIELDS_ALL = [FIELD_ID] - FIELDS_ALL.extend(FIELDS_CHANGE) - FIELDS_TYPING = {} - FIELDS_TRANSLATE = {} + POOL_JOIN_CHAR = '\n' + FIELDS_TRANSLATE_SPECIAL = { + 'dns': 'domain_name_servers', + 'domain': 'domain_name', + 'gateway': 'routers', + 'routes': 'static_routes', + 'tftp_server': 'tftp_server_name', + 'tftp_file': 'boot_file_name', + } EXIST_ATTR = 'subnet' def __init__(self, module: AnsibleModule, result: dict, session: Session = None): @@ -33,14 +52,54 @@ def __init__(self, module: AnsibleModule, result: dict, session: Session = None) self.subnet = {} self.existing_subnets = None - def check(self) -> None: - if self.p['state'] == 'present': - if is_unset(self.p['subnet']): - self.m.fail_json( - "You need to provide the 'subnet' you want to create. E.g. (192.168.1.0/24)!" - ) + def _simplify_existing(self, entry: dict) -> dict: + simple = simplify_translate( + existing=entry, + typing=self.FIELDS_TYPING, + translate=self.FIELDS_TRANSLATE, + ignore=self.API_FIELDS_OPTIONS, + ) + + simple['pools'] = simple['pools'].split(self.POOL_JOIN_CHAR) + opts = entry[self.API_ATTR_OPTIONS] + if isinstance(opts, dict): + simple['dns'] = get_selected_list(opts[self.FIELDS_TRANSLATE_SPECIAL['dns']]) + simple['domain_search'] = get_selected_list(opts['domain_search']) + simple['gateway'] = get_selected_list(opts[self.FIELDS_TRANSLATE_SPECIAL['gateway']]) + simple['routes'] = opts[self.FIELDS_TRANSLATE_SPECIAL['routes']] + simple['domain'] = opts[self.FIELDS_TRANSLATE_SPECIAL['domain']] + simple['ntp_servers'] = get_selected_list(opts['ntp_servers']) + simple['time_servers'] = get_selected_list(opts['time_servers']) + simple['tftp_server'] = opts[self.FIELDS_TRANSLATE_SPECIAL['tftp_server']] + simple['tftp_file'] = opts[self.FIELDS_TRANSLATE_SPECIAL['tftp_file']] + + else: + opt_keys = list(self.FIELDS_TRANSLATE_SPECIAL.keys()) + opt_keys.extend(['domain_search', 'ntp_servers', 'time_servers']) + + for opt in opt_keys: + if opt in self.FIELDS_TYPING['list']: + simple[opt] = [] + + else: + simple[opt] = '' + + return simple + + def _build_request(self) -> dict: + raw_request = self.b.build_request(ignore_fields=self.API_FIELDS_OPTIONS) - if is_unset(self.p['pools']): - self.m.fail_json("You need to provide the IP 'pools' to be used in the subnet!") + raw_request[self.API_KEY]['pools'] = self.POOL_JOIN_CHAR.join(self.p['pools']) + raw_request[self.API_KEY][self.API_ATTR_OPTIONS] = { + self.FIELDS_TRANSLATE_SPECIAL['dns']: self.b.RESP_JOIN_CHAR.join(self.p['dns']), + self.FIELDS_TRANSLATE_SPECIAL['gateway']: self.b.RESP_JOIN_CHAR.join(self.p['gateway']), + self.FIELDS_TRANSLATE_SPECIAL['routes']: self.p['routes'], + self.FIELDS_TRANSLATE_SPECIAL['domain']: self.p['domain'], + self.FIELDS_TRANSLATE_SPECIAL['tftp_server']: self.p['tftp_server'], + self.FIELDS_TRANSLATE_SPECIAL['tftp_file']: self.p['tftp_file'], + 'ntp_servers': self.b.RESP_JOIN_CHAR.join(self.p['ntp_servers']), + 'time_servers': self.b.RESP_JOIN_CHAR.join(self.p['time_servers']), + 'domain_search': self.b.RESP_JOIN_CHAR.join(self.p['domain_search']), + } - self._base_check() + return raw_request diff --git a/plugins/modules/dhcp_subnet.py b/plugins/modules/dhcp_subnet.py index 6df320c..ca8f30e 100644 --- a/plugins/modules/dhcp_subnet.py +++ b/plugins/modules/dhcp_subnet.py @@ -27,9 +27,73 @@ def run_module(): module_args = dict( - subnet=dict(type='str', required=True, description='Subnet to create'), - description=dict(type='str', required=False), - pools=dict(type='str', required=True, description='IP address pools to offer in the subnet'), + subnet=dict( + type='str', required=True, + description='Subnet to use, should be large enough to hold the specified pools and reservations', + ), + description=dict( + type='str', required=False, aliases=['desc'], default='', + ), + pools=dict( + type='list', elements='str', required=False, default=[], + description='List of pools, one per line in range or subnet format ' + '(e.g. 192.168.0.100 - 192.168.0.200 , 192.0.2.64/26)' + ), + auto_options=dict( + type='bool', required=False, default=True, aliases=['option_data_autocollect'], + description='Automatically update option data for relevant attributes as routers, ' + 'dns servers and ntp servers when applying settings from the gui.' + ), + gateway=dict( + type='list', elements='str', required=False, aliases=['gw', 'routers'], default=[], + description='Default gateways to offer to the clients', + ), + routes=dict( + type='str', required=False, aliases=['static_routes'], default='', + description='Static routes that the client should install in its routing cache, ' + 'defined as dest-ip1,router-ip1;dest-ip2,router-ip2', + ), + dns=dict( + type='list', elements='str', required=False, aliases=['dns_servers', 'dns_srv'], default=[], + description='DNS servers to offer to the clients', + ), + domain=dict( + type='str', required=False, aliases=['domain_name', 'dom_name', 'dom'], default='', + description="The domain name to offer to the client, set to this firewall's domain name when left empty", + ), + domain_search=dict( + type='list', elements='str', required=False, aliases=['dom_search'], default=[], + description="Specifies a ´search list´ of Domain Names to be used by the client to locate " + 'not-fully-qualified domain names.', + ), + ntp_servers=dict( + type='list', elements='str', required=False, aliases=['ntp_srv', 'ntp'], default=[], + description='Specifies a list of IP addresses indicating NTP (RFC 5905) servers available to the client.', + ), + time_servers=dict( + type='list', elements='str', required=False, aliases=['time_srv'], default=[], + description='Specifies a list of RFC 868 time servers available to the client.', + ), + next_server=dict( + type='str', required=False, aliases=['next_srv'], default='', + description='Next server IP address', + ), + tftp_server=dict( + type='str', required=False, aliases=['tftp', 'tftp_srv', 'tftp_server_name'], default='', + description='TFTP server address or fqdn', + ), + tftp_file=dict( + type='str', required=False, aliases=['tftp_boot_file', 'boot_file_name'], default='', + description='Boot filename to request', + ), + ipv=dict(type='int', required=False, default=4, choices=[4, 6], aliases=['ip_version']), + match_fields=dict( + type='list', required=False, elements='str', + description='Fields that are used to match configured interface with the running config - ' + "if any of those fields are changed, the module will think it's a new entry", + choices=['subnet', 'description'], + default=['subnet'], + ), **RELOAD_MOD_ARG, **STATE_MOD_ARG, **OPN_MOD_ARGS, @@ -48,6 +112,9 @@ def run_module(): supports_check_mode=True, ) + if module.params['ipv'] == 6: + module.fail_json('DHCPv6 is not yet supported!') + module_wrapper(SubnetV4(module=module, result=result)) module.exit_json(**result) diff --git a/plugins/modules/list.py b/plugins/modules/list.py index 92e7d17..bbbe4dc 100644 --- a/plugins/modules/list.py +++ b/plugins/modules/list.py @@ -423,7 +423,6 @@ def run_module(): from ansible_collections.ansibleguy.opnsense.plugins.module_utils.main.dhcp_subnet_v4 import \ SubnetV4 as Target_Obj - except AttributeError: module_dependency_error() diff --git a/scripts/test.sh b/scripts/test.sh index 64a98f9..4027496 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -154,6 +154,7 @@ run_test 'nginx_upstream_server' 1 run_test 'dhcrelay_destination' 1 run_test 'dhcrelay_relay' 1 run_test 'dhcp_controlagent' 1 +run_test 'dhcp_subnet' 1 run_test 'dhcp_reservation' 1 run_test 'system' 1 run_test 'acme_general' 1 diff --git a/tests/1_cleanup.yml b/tests/1_cleanup.yml index b664c03..a3021ed 100644 --- a/tests/1_cleanup.yml +++ b/tests/1_cleanup.yml @@ -717,3 +717,12 @@ - 'ANSIBLE_TEST_1_9' - 'ANSIBLE_TEST_1_10' - 'ANSIBLE_TEST_DUMMY_1_1' + + - name: Cleanup DHCP Subnets + ansibleguy.opnsense.dhcp_subnet: + subnet: "{{ item }}" + state: 'absent' + loop: + - '192.168.69.0/24' + - '192.168.88.0/24' + - '192.168.89.0/24' diff --git a/tests/dhcp_reservation.yml b/tests/dhcp_reservation.yml index a3d1769..f76c623 100644 --- a/tests/dhcp_reservation.yml +++ b/tests/dhcp_reservation.yml @@ -13,8 +13,6 @@ target: 'dhcp_reservation' tasks: - # todo: add subnet via module - - name: Listing ansibleguy.opnsense.list: register: opn_pre1 @@ -61,6 +59,11 @@ failed_when: not opn_fail4.failed when: not ansible_check_mode + - name: Adding dummy DHCP-subnet + ansibleguy.opnsense.dhcp_subnet: + subnet: '192.168.69.0/24' + check_mode: false + - name: Adding 1 ansibleguy.opnsense.dhcp_reservation: subnet: '192.168.69.0/24' @@ -139,6 +142,12 @@ - '192.168.69.86' when: not ansible_check_mode + - name: Cleanup dummy DHCP-subnet + ansibleguy.opnsense.dhcp_subnet: + subnet: '192.168.69.0/24' + state: 'absent' + check_mode: false + - name: Listing ansibleguy.opnsense.list: register: opn_clean1 diff --git a/tests/dhcp_subnet.yml b/tests/dhcp_subnet.yml new file mode 100644 index 0000000..e47534d --- /dev/null +++ b/tests/dhcp_subnet.yml @@ -0,0 +1,133 @@ +--- + +- name: DHCP Subnet + 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: 'dhcp_subnet' + + tasks: + - name: Listing + ansibleguy.opnsense.list: + register: opn_pre1 + failed_when: > + 'data' not in opn_pre1 or + opn_pre1.data | length != 0 + + - name: Removing - does not exist + ansibleguy.opnsense.dhcp_subnet: + subnet: '192.168.88.0/24' + state: 'absent' + register: opn_pre2 + failed_when: > + opn_pre2.failed or + opn_pre2.changed + + # - name: Adding 1 - failing because of non-existent subnet + # ansibleguy.opnsense.dhcp_subnet: + # subnet: '192.168.59.0/24' + # ip: '192.168.59.76' + # register: opn_fail1 + # failed_when: not opn_fail1.failed + + - name: Adding 1 + ansibleguy.opnsense.dhcp_subnet: + subnet: '192.168.88.0/24' + register: opn1 + failed_when: > + opn1.failed or + not opn1.changed + + - name: Adding 2 + ansibleguy.opnsense.dhcp_subnet: + subnet: '192.168.89.0/24' + pools: + - '192.168.89.10-192.168.89.20' + - '192.168.89.40-192.168.89.50' + auto_options: false + gateway: '192.168.89.200' + dns: '1.1.1.1' + domain: 'test.lan' + register: opn2 + failed_when: > + opn2.failed or + not opn2.changed + + - name: Changing 2 + ansibleguy.opnsense.dhcp_subnet: + subnet: '192.168.89.0/24' + pools: + - '192.168.89.40-192.168.89.60' + - '192.168.89.110-192.168.89.120' + auto_options: false + gateway: '192.168.89.240' + dns: ['1.1.1.2', '1.0.0.2'] + domain: 'test.lan' + time_servers: '192.168.89.240' + tftp_server: '192.168.89.210' + tftp_file: 'openwrt.bin' + register: opn3 + failed_when: > + opn3.failed or + not opn3.changed + when: not ansible_check_mode + + - name: Changing 2 - Nothing changed + ansibleguy.opnsense.dhcp_subnet: + subnet: '192.168.89.0/24' + pools: + - '192.168.89.40-192.168.89.60' + - '192.168.89.110-192.168.89.120' + auto_options: false + gateway: '192.168.89.240' + dns: ['1.1.1.2', '1.0.0.2'] + domain: 'test.lan' + time_servers: '192.168.89.240' + tftp_server: '192.168.89.210' + tftp_file: 'openwrt.bin' + register: opn4 + failed_when: > + opn4.failed or + opn4.changed + when: not ansible_check_mode + + - name: Listing + ansibleguy.opnsense.list: + register: opn8 + failed_when: > + 'data' not in opn8 or + opn8.data | length != 2 + when: not ansible_check_mode + + - name: Removing 2 + ansibleguy.opnsense.dhcp_subnet: + subnet: '192.168.89.0/24' + state: absent + register: opn5 + failed_when: > + opn5.failed or + not opn5.changed + when: not ansible_check_mode + + - name: Cleanup + ansibleguy.opnsense.dhcp_subnet: + subnet: "{{ item }}" + state: 'absent' + loop: + - '192.168.88.0/24' + - '192.168.89.0/24' + 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 != 0 + when: not ansible_check_mode From 42b727db6ac95579866bec61a1c43a10d182b00b Mon Sep 17 00:00:00 2001 From: Rath Pascal Date: Sat, 11 Jan 2025 19:38:00 +0100 Subject: [PATCH 3/4] implement dhcp_subnet module --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 44f45db..fc075dd 100644 --- a/README.md +++ b/README.md @@ -190,6 +190,7 @@ not implemented => development => [testing](https://github.com/ansibleguy/collec | **Nginx** | ansibleguy.opnsense.nginx_upstream_server | [Docs](https://opnsense.ansibleguy.net/modules/nginx.html#ansibleguy-opnsense-nginx-upstream-server) | unstable | | **DHCP Relay** | ansibleguy.opnsense.dhcrelay | [Docs](https://opnsense.ansibleguy.net/modules/dhcrelay_relay.html) | unstable | | **DHCP Relay** | ansibleguy.opnsense.dhcrelay_destination | [Docs](https://opnsense.ansibleguy.net/modules/dhcrelay_destination.html) | unstable | +| **DHCP Subnet** | ansibleguy.opnsense.dhcp_subnet | [Docs](https://opnsense.ansibleguy.net/modules/dhcp.html) | unstable | | **DHCP Reservation** | ansibleguy.opnsense.dhcp_reservation | [Docs](https://opnsense.ansibleguy.net/modules/dhcp.html) | unstable | | **DHCP Controlagent** | ansibleguy.opnsense.dhcp_controlagent | [Docs](https://opnsense.ansibleguy.net/modules/dhcp.html) | unstable | | **ACME (Certificates)** | ansibleguy.opnsense.acme_account | [Docs](https://opnsense.ansibleguy.net/modules/acmeclient.html) | unstable | From 5d2d500021856684e3bbdeb9be987ce28379f31e Mon Sep 17 00:00:00 2001 From: Rath Pascal Date: Sat, 11 Jan 2025 19:41:51 +0100 Subject: [PATCH 4/4] lint fix --- plugins/modules/list.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/modules/list.py b/plugins/modules/list.py index bbbe4dc..9e05a23 100644 --- a/plugins/modules/list.py +++ b/plugins/modules/list.py @@ -36,8 +36,8 @@ 'ipsec_child', 'ipsec_vti', 'ipsec_auth_local', 'ipsec_auth_remote', 'frr_general', 'unbound_general', 'unbound_acl', 'ids_general', 'ids_policy', 'ids_rule', 'ids_ruleset', 'ids_user_rule', 'ids_policy_rule', 'openvpn_instance', 'openvpn_static_key', 'openvpn_client_override', 'dhcrelay_destination', 'dhcrelay_relay', - 'interface_lagg', 'interface_loopback', 'unbound_dnsbl', 'dhcp_reservation', 'dhcp_subnet', 'acme_general', 'acme_account', - 'acme_validation', 'acme_action', 'acme_certificate', + 'interface_lagg', 'interface_loopback', 'unbound_dnsbl', 'dhcp_reservation', 'dhcp_subnet', 'acme_general', + 'acme_account', 'acme_validation', 'acme_action', 'acme_certificate', ]