aboutsummaryrefslogtreecommitdiffstats
path: root/tools/dhcpns/main.py
blob: e826a5fac9f3d78ba7e4690851d99d50df6b9ef8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
import pynetbox
import os
from dotenv import load_dotenv
import json
from pdns import PowerDNS
import ipaddress
import subprocess
import re
import netaddr

from config.dhcp4 import base as dhcp4
from config.dhcp6 import base as dhcp6
from config.dhcp4 import fap
from config.ddns import base as ddns
from config.ddns import ddns_domain
from config.dhcp4 import subnet as subnet4
from config.dhcp6 import subnet as subnet6


# Take environment variables from .env
load_dotenv()

DOMAIN_NAME = os.environ['DOMAIN_NAME']
DOMAIN_SEARCH = os.environ['DOMAIN_SEARCH']
NAMESERVERS = os.environ['NAMESERVERS'].split()

nb = pynetbox.api(
    os.getenv('NETBOX_URL'),
    token=os.getenv('NETBOX_API_KEY'),
    threading=True,
)

# DNS
pdns = PowerDNS(os.environ['PDNS_API_URL'], os.environ['PDNS_API_KEY'])

# Load all zones to later check if a zone already exist
zones = [zone['name'] for zone in pdns.list_zones()]

rdns_zones = pdns.search("*.arpa", 2000, "zone")

kea4_subnets = []
kea6_subnets = []
kea_ddns_domains = []
kea_rddns_domains = []

# dhcp-client
vlans = nb.ipam.vlans.filter(tag='dhcp-client')
for vlan in vlans:
    vlan_domain_name = f"net-{vlan.name}.{DOMAIN_NAME}"
    prefixes4 = []
    prefixes6 = []
    kea_ddns_domains.append(ddns_domain(vlan_domain_name))

    for prefix in nb.ipam.prefixes.filter(vlan_id=vlan.id, family=4):
        kea4_subnets.append(
            subnet4(vlan, prefix, DOMAIN_NAME, vlan_domain_name))
        prefixes4.append(prefix)

    for prefix in nb.ipam.prefixes.filter(vlan_id=vlan.id, family=6):
        kea6_subnets.append(
            subnet6(vlan, prefix, DOMAIN_NAME, vlan_domain_name))
        prefixes6.append(prefix)

    if f"{vlan_domain_name}." not in zones and len(prefixes4) >= 1:
        print(pdns.create_zone(f"{vlan_domain_name}.", NAMESERVERS))
        print(pdns.create_zone_metadata(
            f"{vlan_domain_name}.", 'TSIG-ALLOW-DNSUPDATE', 'dhcpns'))

        zone_rrsets = []

        for prefix in prefixes4:
            network = ipaddress.ip_network(prefix)

            # Network ID
            zone_rrsets.append({'name': f'id-{network[0]}.{vlan_domain_name}.', 'changetype': 'replace', 'type': 'A', 'records': [
                {'content': str(network[0]), 'disabled': False, 'type':'A'}], 'ttl': 900})

            # Gateway
            zone_rrsets.append({'name': f'gw-{network[1]}.{vlan_domain_name}.', 'changetype': 'replace', 'type': 'A', 'records': [
                {'content': str(network[1]), 'disabled': False, 'type':'A'}], 'ttl': 900})

            # Broadcast
            zone_rrsets.append({'name': f'broadcast-{network[-1]}.{vlan_domain_name}.', 'changetype': 'replace', 'type': 'A', 'records': [
                {'content': str(network[-1]), 'disabled': False, 'type':'A'}], 'ttl': 900})

            # Apply zone_rrsets
            pdns.set_records(vlan_domain_name, zone_rrsets)

            rdns_zone = pdns.get_rdns_zone_from_ip(network[0])
            rdns_rrsets = []
            if rdns_zone is None:
                print(f"Failed to find RDNS Zone for IP {network[0]}")

            # Network ID
            rdns_rrsets.append({"name": network[0].reverse_pointer + '.', "changetype": "replace", "type": "PTR", "records": [
                {"content": f'net-{network[0]}.{vlan_domain_name}.', "disabled": False, "type": "PTR"}], "ttl": 900})

            # Gateway
            rdns_rrsets.append({"name": network[1].reverse_pointer + '.', "changetype": "replace", "type": "PTR", "records": [
                {"content": f'gw-{network[1]}.{vlan_domain_name}.', "disabled": False, "type": "PTR"}], "ttl": 900})

            # Broadcast
            rdns_rrsets.append({"name": network[-1].reverse_pointer + '.', "changetype": "replace", "type": "PTR", "records": [
                {"content": f'broadcast-{network[-1]}.{vlan_domain_name}.', "disabled": False, "type": "PTR"}], "ttl": 900})

            # Apply rdns_rrsets
            pdns.set_records(network[1].reverse_pointer + '.', rdns_rrsets)

# dhcp-mgmt-edge
vlans = nb.ipam.vlans.filter(tag='dhcp-mgmt-edge')
for vlan in vlans:
    prefixes4 = []
    for prefix in nb.ipam.prefixes.filter(vlan_id=vlan.id, family=4):
        kea4_subnets.append(
            fap(vlan, prefix))


for zone in rdns_zones:
    kea_rddns_domains.append(ddns_domain(zone['name'][:-1]))

# Write DDNS
if os.environ['KEA_DDNS_FILE'] is not None:
    with open(os.environ['KEA_DDNS_FILE'], "w") as outfile:
        outfile.write(json.dumps(
            {"DhcpDdns": ddns(kea_ddns_domains, kea_rddns_domains)}, indent=2))

# Write DHCPv4
if os.environ['KEA_DHCP4_FILE'] is not None:
    with open(os.environ['KEA_DHCP4_FILE'], "w") as outfile:
        outfile.write(json.dumps({"Dhcp4": dhcp4(kea4_subnets)}, indent=2))

# Write DHCPv6
if os.environ['KEA_DHCP6_FILE'] is not None:
    with open(os.environ['KEA_DHCP6_FILE'], "w") as outfile:
        outfile.write(json.dumps({"Dhcp6": dhcp6(kea6_subnets)}, indent=2))

# Test DHCPv4
try:
    subprocess.check_call(['/usr/sbin/kea-dhcp4', '-t',
                          os.environ['KEA_DHCP4_FILE']])
except subprocess.CalledProcessError:
    print("Failed to validate kea-dhcp4 config. What do we do now?")

# Test DHCPv6
try:
    subprocess.check_call(['/usr/sbin/kea-dhcp6', '-t',
                          os.environ['KEA_DHCP6_FILE']])
except subprocess.CalledProcessError:
    print("Failed to validate kea-dhcp6 config. What do we do now?")


# Reload all zones
zones = [zone['name'] for zone in pdns.list_zones()]

# Create DNS for devices
devices = nb.dcim.devices.all()
for device in devices:
    if device.primary_ip4 is None or device.primary_ip6 is None:
        continue

    zone = "tg23.gathering.org"

    # IPv4
    zone_rrsets = []
    if device.primary_ip4 is not None:
        zone_rrsets.append({'name': f'{device.name}.{zone}.', 'changetype': 'replace', 'type': 'A', 'records': [
            {'content': str(netaddr.IPNetwork(str(device.primary_ip4)).ip), 'disabled': False, 'type': 'A'}], 'ttl': 900})

    # IPv6    
    if device.primary_ip6 is not None:
        zone_rrsets.append({'name': f'{device.name}.{zone}.', 'changetype': 'replace', 'type': 'AAAA', 'records': [
            {'content': str(netaddr.IPNetwork(str(device.primary_ip6)).ip), 'disabled': False, 'type': 'A'}], 'ttl': 900})

    if len(zone_rrsets) > 1:
        # Apply zone_rrsets
        print(pdns.set_records(zone, zone_rrsets))

        rdns_zone = pdns.get_rdns_zone_from_ip(
            str(netaddr.IPNetwork(str(device.primary_ip4)).ip))
        rdns_rrsets = []
        if rdns_zone is None:
            print(f"Failed to find RDNS Zone for IP")

    # IPv4 RDNS
    rdns_rrsets.append({"name": ipaddress.ip_address(str(netaddr.IPNetwork(str(device.primary_ip4)).ip)).reverse_pointer + '.', "changetype": "replace", "type": "PTR", "records": [
        {"content": f'{device.name}.{zone}.', "disabled": False, "type": "PTR"}], "ttl": 900})

    # Apply rdns_rrsets
    print(pdns.set_records(rdns_zone, rdns_rrsets))