From bbf0aa2090d043b791c025136f11f3115e2de261 Mon Sep 17 00:00:00 2001 From: Saad Ali Date: Sat, 16 Sep 2023 22:25:07 +0500 Subject: [PATCH] Modified keahook.py to update PowerDNS for host reservation changes --- docker/kea/Dockerfile | 3 +- docker/kea/keahook.py | 139 +++++++++++++++++++++++++++++++++++++----- 2 files changed, 125 insertions(+), 17 deletions(-) diff --git a/docker/kea/Dockerfile b/docker/kea/Dockerfile index 36a5f55..c12a2ed 100644 --- a/docker/kea/Dockerfile +++ b/docker/kea/Dockerfile @@ -17,6 +17,7 @@ RUN set -eux; \ socat \ python3 \ python3-pip \ + python3-requests \ libpq5 \ libmariadb3 \ libpython3.11 \ @@ -58,7 +59,7 @@ RUN set -eux; \ --disable-rpath \ --enable-generate-parser \ --without-werror; \ - make -j; \ + make; \ make install # Install kea_python https://github.com/invite-networks/kea_python diff --git a/docker/kea/keahook.py b/docker/kea/keahook.py index 29b6ee1..c86a37a 100644 --- a/docker/kea/keahook.py +++ b/docker/kea/keahook.py @@ -1,5 +1,14 @@ +from os import environ import kea +import json +import requests +powerdns_domain = environ["PDNS_DOMAIN"] +powerdns_api_key = environ["PDNS_API_KEY"] +powerdns_api_host = environ["PDNS_API_HOST"] +powerdns_api_port = environ["PDNS_API_PORT"] +powerdns_api_protocol = environ["PDNS_API_PROTOCOL"] +powerdns_api_url = f"{powerdns_api_protocol}://{powerdns_api_host}:{powerdns_api_port}/api/v1" class UNSPECIFIED: pass @@ -66,6 +75,91 @@ def wrap_handler(handle, get_response): return 1 return 0 +# Add dns record to PowerDNS via its API +def add_dns_record(host_ip, hostname): + headers = {'X-API-Key': powerdns_api_key} + payload = { + "rrsets": [ + { + "name": f"{hostname}.", + "type": "A", + "ttl": 600, + "changetype": "REPLACE", + "records": [ + { + "content": host_ip, + "disabled": False + } + ] + } + ] + } + + response = requests.patch( + f"{powerdns_api_url}/servers/localhost/zones/{powerdns_domain}.", + headers=headers, + data=json.dumps(payload) + ) + + if response.status_code not in [200, 201, 204]: + raise Exception(f"Failed to add DNS record: {response.content}") + +# Delete dns record from PowerDNS via its API +def delete_dns_record(hostname): + headers = { 'X-API-Key': powerdns_api_key } + + data = { + "rrsets": [ + { + "name": f"{hostname}.{powerdns_domain}.", + "type": "A", + "changetype": "DELETE" + } + ] + } + + response = requests.patch( + f"{powerdns_api_url}/servers/localhost/zones/{powerdns_domain}.", + headers=headers, + data=json.dumps(data) + ) + + if response.status_code not in [200, 204]: + raise Exception(f"Failed to delete DNS record: {response.content}") + + +def core_add_reservation(subnet_id, resv, host_mgr): + unsupported_arguments = [ 'identifier-type' ] + # Remove any unsupported keys from the reservation dictionary + for key in unsupported_arguments: + resv.pop(key, None) + host = kea.HostReservationParser4().parse(subnet_id, resv) + host_mgr.add(host) + ip_address = resv.get('ip-address') + hostname = resv.get('hostname') + fqdn = f"{hostname}.{powerdns_domain}" + try: + add_dns_record(ip_address, fqdn) + except Exception as e: + return {'result': 1, 'text': f"Failed to add DNS record: {e}"} + return {'result': 0, 'text': f"Host reservation added: MAC={resv.get('hw-address')}, IP={ip_address}"} + +def core_del_reservation(subnet_id, args, host_mgr): + identifier_type = args.get('identifier-type') + # Explicitly get 'identifier' or 'hw-address' + identifier = args.get('identifier') or args.get('hw-address') + if identifier is None: + return {'result': 1, 'text': f"Identifier not provided for identifier type {identifier_type}"} + + was_deleted = host_mgr.del4(subnet_id, identifier_type, identifier) + if was_deleted: + hostname = args.get('hostname') + try: + delete_dns_record(hostname) + except Exception as e: + return {'result': 1, 'text': f"Failed to delete DNS record: {e}"} + return {'result': 0, 'text': 'Host deleted.'} + return {'result': 1, 'text': 'Host not deleted (not found).'} # {"command": "reservation-add", # "arguments": {"reservation": {"subnet-id": 1, @@ -75,11 +169,8 @@ def get_response(args): resv = get_map_arg(args, 'reservation') subnet_id = get_int_arg(resv, 'subnet-id') del resv['subnet-id'] - host = kea.HostReservationParser4().parse(subnet_id, resv) - kea.HostMgr.instance().add(host) - return {'result': 0, - 'text': 'Host added.'} - + host_mgr = kea.HostMgr.instance() + return core_add_reservation(subnet_id, resv, host_mgr) return wrap_handler(handle, get_response) @@ -164,18 +255,33 @@ def get_response(args): # "identifier": "01:02:03:04:05:06"}} def reservation_del(handle): def get_response(args): - host_mgr = kea.HostMgr.instance() subnet_id = get_int_arg(args, 'subnet-id') - if 'ip-address' in args: - ip_address = get_string_arg(args, 'ip-address') - was_deleted = host_mgr.del_(subnet_id, ip_address) - else: - identifier_type = get_string_arg(args, 'identifier-type') - identifier = get_string_arg(args, 'identifier') - was_deleted = host_mgr.del4(subnet_id, identifier_type, identifier) - if was_deleted: - return {'result': 0, 'text': 'Host deleted.'} - return {'result': 1, 'text': 'Host not deleted (not found).'} + host_mgr = kea.HostMgr.instance() + return core_del_reservation(subnet_id, args, host_mgr) + return wrap_handler(handle, get_response) + +def reservation_update(handle): + def get_response(args): + resv = get_map_arg(args, 'reservation') + subnet_id = get_int_arg(resv, 'subnet-id') + del resv['subnet-id'] + host_mgr = kea.HostMgr.instance() + + hw_address = resv.get('hw-address') + if hw_address is None: + return {'result': 1, 'text': 'hw-address not provided'} + + # Ensure that hw_address is a string + if not isinstance(hw_address, str): + return {'result': 1, 'text': 'hw-address must be a string'} + + # First delete the old reservation + del_response = core_del_reservation(subnet_id, {'identifier-type': 'hw-address', 'identifier': hw_address}, host_mgr) + if del_response['result'] != 0: + return del_response + + # Then add the new reservation + return core_add_reservation(subnet_id, resv, host_mgr) return wrap_handler(handle, get_response) @@ -186,4 +292,5 @@ def load(handle): handle.registerCommandCallout('reservation-get-all', reservation_get_all) handle.registerCommandCallout('reservation-get-page', reservation_get_page) handle.registerCommandCallout('reservation-del', reservation_del) + handle.registerCommandCallout('reservation_update', reservation_update) return 0