Skip to content

Commit

Permalink
implement dhcp_reservation module (#45)
Browse files Browse the repository at this point in the history
  • Loading branch information
ansibleguy committed Nov 1, 2024
1 parent 9099a27 commit 631f5cc
Show file tree
Hide file tree
Showing 11 changed files with 436 additions and 14 deletions.
93 changes: 93 additions & 0 deletions docs/source/modules/dhcp.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
.. _modules_dhcp:

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

====
DHCP
====

**STATE**: unstable

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

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

**Service Docs**: `DHCP <https://docs.opnsense.org/manual/dhcp.html#kea-dhcp>`_

Contribution
************

Thanks to `@KalleDK <https://github.com/KalleDK>`_ for helping with the Reservation module!

----

Definition
**********

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

ansibleguy.opnsense.dhcp_reservation
====================================

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

"ip","string","true","","ip_address","IP address to offer to the client"
"mac","string","false for state changes, else true","","mac_address","MAC/Ether address of the client in question"
"subnet","string","false for state changes, else true","","\-","Subnet this reservation belongs to"
"hostname","string","false","","\-","Offer a hostname to the client"
"description","string","false","","\-","Optional description"
"reload","boolean","false","true","\-", .. include:: ../_include/param_reload.rst

----

Examples
********

ansibleguy.opnsense.dhcp_reservation
====================================

.. 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: 'dhcp_reservation'
tasks:
- name: Example
ansibleguy.opnsense.dhcp_reservation:
ip: '192.168.0.1'
subnet: '192.168.0.0/24'
mac: 'aa:aa:aa:bb:bb:bb'
# hostname: 'test'
# description: ''
# state: 'present'
# reload: true
# debug: false
- name: Adding
ansibleguy.opnsense.dhcp_reservation:
subnet: '192.168.0.0/24'
ip: '192.168.0.1'
mac: 'aa:aa:aa:bb:bb:bb'
- name: Removing
ansibleguy.opnsense.dhcp_reservation:
ip: '192.168.0.1'
state: 'absent'
- name: Listing
ansibleguy.opnsense.list:
# target: 'dhcp_reservation'
register: existing_entries
- name: Show existing reservations
ansible.builtin.debug:
var: existing_entries.data
2 changes: 1 addition & 1 deletion docs/source/modules/nginx.rst
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ Definition
.. include:: ../_include/param_basic.rst

ansibleguy.opnsense.nginx_general
=========================================
=================================

.. csv-table:: Definition
:header: "Parameter", "Type", "Required", "Default", "Aliases", "Comment"
Expand Down
3 changes: 3 additions & 0 deletions meta/runtime.yml
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,8 @@ action_groups:
dhcrelay:
- ansibleguy.opnsense.dhcrelay_destination
- ansibleguy.opnsense.dhcrelay_relay
dhcp:
- ansibleguy.opnsense.dhcp_reservation
all:
- metadata:
extend_group:
Expand All @@ -147,6 +149,7 @@ action_groups:
- ansibleguy.opnsense.ids
- ansibleguy.opnsense.openvpn
- ansibleguy.opnsense.dhcrelay
- ansibleguy.opnsense.dhcp

plugin_routing:
modules:
Expand Down
26 changes: 14 additions & 12 deletions plugins/module_utils/helper/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,28 +72,30 @@ def is_ip6(host: str, ignore_empty: bool = False, strip_enclosure: bool = True)
return False


def is_network(entry: str, strict: bool = False) -> bool:
try:
ip_network(entry, strict=strict)
return True

except ValueError:
return False


def is_ip_or_network(entry: str, strict: bool = False) -> bool:
valid = is_ip(entry)

if not valid:
try:
ip_network(entry, strict=strict)
valid = True

except ValueError:
valid = False
if valid:
return valid

return valid
return is_network(entry=entry, strict=strict)


def is_ip6_network(entry: str, strict: bool = False) -> bool:
try:
valid = isinstance(ip_network(entry, strict=strict), IPv6Network)
return isinstance(ip_network(entry, strict=strict), IPv6Network)

except ValueError:
valid = False

return valid
return False


def valid_hostname(name: str) -> bool:
Expand Down
77 changes: 77 additions & 0 deletions plugins/module_utils/main/dhcp_reservation_v4.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
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 ReservationV4(BaseModule):
FIELD_ID = 'ip'
CMDS = {
'add': 'addReservation',
'del': 'delReservation',
'set': 'setReservation',
'search': 'searchReservation',
'detail': 'getReservation',
}
API_KEY_PATH = 'reservation'
API_MOD = 'kea'
API_CONT = 'dhcpv4'
API_CONT_REL = 'service'
API_CMD_REL = 'reconfigure'
FIELDS_CHANGE = [
'mac', 'hostname', 'description', 'subnet'
]
FIELDS_ALL = [FIELD_ID]
FIELDS_ALL.extend(FIELDS_CHANGE)
FIELDS_TYPING = {
'select': ['subnet'],
}
FIELDS_TRANSLATE = {
'ip': 'ip_address',
'mac': 'hw_address',
}
FIELDS_IGNORE = ['subnet'] # empty field ?!
EXIST_ATTR = 'reservation'

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

def check(self) -> None:
if self.p['state'] == 'present':
if is_unset(self.p['mac']):
self.m.fail_json(
"You need to provide a 'mac' if you want to create a reservation!"
)

if is_unset(self.p['subnet']) or not is_network(self.p['subnet']):
self.m.fail_json('The provided subnet is invalid!')

if not is_ip(self.p['ip']):
self.m.fail_json('The provided IP is invalid!')

self._base_check()

if self.p['state'] == 'present':
self._search_subnets()
if not self._find_subnet():
self.m.fail_json('Provided subnet not found!')

def _find_subnet(self) -> bool:
for s in self.existing_subnets:
if s['subnet'] == self.p['subnet']:
self.p['subnet'] = s['uuid']
self.reservation['subnet'] = s['uuid']
return True

return False

def _search_subnets(self):
self.existing_subnets = self.s.get(cnf={
**self.call_cnf, **{'command': 'searchSubnet'}
})['rows']
78 changes: 78 additions & 0 deletions plugins/modules/dhcp_reservation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
#!/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/plugins/wireguard.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_reservation_v4 import ReservationV4

except MODULE_EXCEPTIONS:
module_dependency_error()


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


def run_module():
module_args = dict(
ip=dict(
type='str', required=True, aliases=['ip_address'],
description='IP address to offer to the client',
),
mac=dict(
type='str', required=False, aliases=['mac_address'],
description='MAC/Ether address of the client in question',
),
subnet=dict(
type='str', required=False,
description='Subnet this reservation belongs to',
),
hostname=dict(
type='str', required=False,
description='Offer a hostname to the client',
),
description=dict(type='str', required=False, aliases=['desc']),
ipv=dict(type='int', required=False, default=4, choices=[4, 6], aliases=['ip_version']),
**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,
)

if module.params['ipv'] == 6:
module.fail_json('DHCPv6 is not yet supported!')

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


def main():
run_module()


if __name__ == '__main__':
main()
6 changes: 5 additions & 1 deletion plugins/modules/list.py
Original file line number Diff line number Diff line change
Expand Up @@ -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',
'interface_lagg', 'interface_loopback', 'unbound_dnsbl', 'dhcp_reservation',
]


Expand Down Expand Up @@ -399,6 +399,10 @@ def run_module():
from ansible_collections.ansibleguy.opnsense.plugins.module_utils.main.dhcrelay_relay import \
DhcRelayRelay as Target_Obj

elif target == 'dhcp_reservation':
from ansible_collections.ansibleguy.opnsense.plugins.module_utils.main.dhcp_reservation_v4 import \
ReservationV4 as Target_Obj

except AttributeError:
module_dependency_error()

Expand Down
1 change: 1 addition & 0 deletions scripts/test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ run_test 'nginx_general' 1
run_test 'nginx_upstream_server' 1
run_test 'dhcrelay_destination' 1
run_test 'dhcrelay_relay' 1
run_test 'dhcp_reservation' 1
run_test 'system' 1
run_test 'package' 1

Expand Down
8 changes: 8 additions & 0 deletions tests/cleanup.yml
Original file line number Diff line number Diff line change
Expand Up @@ -661,3 +661,11 @@
ansibleguy.opnsense.dhcrelay_destination:
name: 'ANSIBLE_TEST_1_1'
state: 'absent'

- name: Cleanup DHCP-reservations
ansibleguy.opnsense.dhcp_reservation:
ip: "{{ item }}"
state: 'absent'
loop:
- '192.168.69.76'
- '192.168.69.86'
Loading

0 comments on commit 631f5cc

Please sign in to comment.