forked from HiddenClever/digitalocean-dns-sync
/
sync_dns.py
executable file
·287 lines (243 loc) · 11.2 KB
/
sync_dns.py
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
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
import dns.zone
from dns.rdataclass import *
from dns.rdatatype import *
from dns import rdatatype
import requests
import glob
import json
import os.path
import sys
try:
from sync_dns_settings import ip, auth_token, bindfolder, bindextension
except ImportError:
print >>sys.stderr, "[ERROR] Please copy sync_dns_settings.py.example to sync_dns_settings.py " \
"and adjust the values with the server ip address and your digitalocean api key." \
"Also change the bindfolder and bindextension values if they're different on your server."
exit()
base_url = "https://api.digitalocean.com/v2/domains"
headers = {'Content-Type': 'application/json', 'Authorization': 'Bearer {0}'.format(auth_token)}
def handle_error(response):
msg = None
if hasattr(response, 'status_code'):
if response.status_code == 401:
msg = "\n[ERROR] An authorization error has occurred, please check your auth_token is correct."
elif response.status_code == 404:
# This is a non-fatal error
print "--> Not found, continuing"
return
elif response.status_code == 429:
msg = "\n[ERROR] You have exceeded DigitalOcean's Rate Limit of 1200 requests per hour. " \
"Please wait an hour before trying again and type the last domain synchronised when prompted."
if msg is None:
msg = "\n[ERROR] An unknown error has occurred, please re-run the script."
print >>sys.stderr, msg
if hasattr(response, 'status_code'):
print >>sys.stderr, "Response status code:", response.status_code
try:
content_msg = json.loads(response.content)['message']
print >>sys.stderr, "Response message:", content_msg
except AttributeError:
debug_msg = json.dumps(response, indent=4, sort_keys=True)
print >>sys.stderr, "Response body:", debug_msg
exit()
def qualifyName(dnsName, domain):
dnsName = str(dnsName)
if domain not in dnsName and dnsName != '@':
return dnsName + '.' + domain + '.'
else:
# Catches the @ symbol case too.
return domain + '.'
def check_domain(domain_records_url, domain):
print "\nChecking DigitalOcean DNS for", domain
response = requests.get(domain_records_url, headers=headers)
if response.status_code == 200:
print "--> Domain records found"
elif response.status_code == 404:
print "--> Domain records not found, creating zone"
data = {"name": domain, "ip_address": ip}
response = requests.post(base_url, data=json.dumps(data), headers=headers).json()
if 'domain' in response and 'name' in response['domain'] and response['domain']['name'] == domain:
print "--> Successfully created zone for", domain
# Wipe the default DigitalOcean records
wipe_zone(domain_records_url)
else:
handle_error(response)
else:
handle_error(response)
def sync_zone(domain_records_url, domain):
# Synchronise zone
print "\nSynchronising DNS zone for", domain, "..."
# First get all the existing records
existing_records = requests.get(domain_records_url+"?per_page=9999", headers=headers).json().get('domain_records', [])
# Create an array to hold all the updated records
updated_records = []
# Create an array to hold synchronised record IDs
synced_record_ids = []
# Get the BIND raw DNS dump
bindfile = bindfolder + domain + bindextension
with open(bindfile, "r") as dns_file:
dns_dump = dns_file.read()
dns_dump = "$ORIGIN {0}.\n{1}".format(domain, dns_dump)
zone = dns.zone.from_text(dns_dump)
for name, node in zone.nodes.items():
name = str(name)
print "\nRecord name:", name
print "Qualified name:", qualifyName(name, domain)
rdatasets = node.rdatasets
for rset in rdatasets:
print "--> TTL:", str(rset.ttl)
print "--> Type:", rdatatype.to_text(rset.rdtype)
for rdata in rset:
data = None
priority = None
port = None
weight = None
if rset.rdtype == MX:
priority = rdata.preference
print "--> Priority:", priority
if unicode(rdata.exchange) == "@":
data = "%s." % (domain)
else:
data = "%s.%s." % (rdata.exchange, domain)
elif rset.rdtype == CNAME:
if unicode(rdata) == "@":
data = "@"
else:
data = rdata.target
elif rset.rdtype == A:
data = rdata.address
elif rset.rdtype == AAAA:
data = rdata.address.lower()
elif rset.rdtype == NS:
data = rdata.target
elif rset.rdtype == SRV:
priority = rdata.priority
weight = rdata.weight
port = rdata.port
data = rdata.target
elif rset.rdtype == TXT:
data = " ".join('"{0}"'.format(string) for string in rdata.strings)
if data:
print "--> Data:", data
data = unicode(data)
type = rdatatype.to_text(rset.rdtype)
# Try and find an existing record
record_id = None
for record in existing_records:
if type in ["CNAME", "MX", "NS", "SRV"] and data[-1:] == ".":
check_data = data[:-1]
else:
check_data = data
if record['name'] == name and record['type'] == type and record['data'] == check_data:
record_id = record['id']
synced_record_ids.append(record_id)
break
if record_id:
print "--> Already exists, skipping"
else:
if type in ["CNAME", "MX", "NS", "SRV"] and data != "@" and data[-1:] != ".":
data = "{0}.{1}.".format(data, domain)
post_data = {
"type": type,
"name": name,
"data": data,
"priority": priority,
"port": port,
"weight": weight
}
# Collect records to be updated into the updated_records array
print "--> Queuing to update"
updated_records.append(post_data)
# Delete any records that exist with DigitalOcean that have been removed
print "\nRemoving deleted records"
for record in existing_records:
if record['id'] not in synced_record_ids and record['type'] != 'SOA':
response = requests.delete("{0}/{1}".format(domain_records_url, record["id"]), headers=headers)
if response.status_code == 204:
print "--> Deleted record", record["name"], "IN", record["type"], record["data"]
else:
handle_error(response)
print "--> Done"
# Finally, post the responses for the updated records
print "\nPosting updated records"
for record in updated_records:
response = requests.post(domain_records_url, data=json.dumps(record), headers=headers).json()
if 'domain_record' in response:
print "--> Updated record", record["name"], "IN", record["type"], record["data"]
else:
handle_error(response)
print "--> Done"
print "\n--> Complete\n"
def wipe_zone(domain_records_url):
# Wipe all the existing records for a given domain
print "\nWiping default DigitalOcean records..."
response = requests.get(domain_records_url, headers=headers).json()
# print "Response body:", json.dumps(response, indent=4, sort_keys=True)
if 'domain_records' in response:
for record in response['domain_records']:
response = requests.delete("{0}/{1}".format(domain_records_url, record["id"]), headers=headers)
if response.status_code == 204:
print "--> Deleted record", record["type"], record["data"]
else:
handle_error(response)
print "--> Done"
else:
handle_error(response)
if __name__ == '__main__':
args = sys.argv
if "--delete" in args:
args.remove("--delete")
delete = True
else:
delete = False
if len(args) == 1:
print "You have not specified a domain, would you like to wipe and re-sync all domains in the system?"
sync_all = raw_input("Please type y or n: ")
print "Did you run this previously and reach the DigitalOcean API limit?"
resume_domain = raw_input("If so type the last domain name here to resume: ")
if sync_all == "y":
found = False
for filename in sorted(glob.glob(bindfolder + "*" + bindextension)):
domain = os.path.basename(filename)[:-3]
domain_url = base_url + "/{0}".format(domain)
domain_records_url = "{0}/records".format(domain_url)
if resume_domain:
if resume_domain == domain:
found = True
if not found:
continue
# 1. Delete the domain. We do this because it is quicker than wiping each record individually.
print "\nDeleting", domain, "..."
response = requests.delete(domain_url, headers=headers)
if response.status_code == 204:
print "--> Done"
else:
handle_error(response)
# 2. Re-create the domain
check_domain(domain_records_url, domain)
# 3. Sync zone
sync_zone(domain_records_url, domain)
else:
exit()
elif len(args) == 2:
domain_url = base_url + "/{0}".format(args[1])
if delete:
print "\nDeleting", args[1], "..."
response = requests.delete(domain_url, headers=headers)
if response.status_code == 204:
print "--> Done"
else:
handle_error(response)
else:
domainfile = bindfolder + args[1] + bindextension
if os.path.isfile(domainfile):
domain_records_url = "{0}/records".format(domain_url)
check_domain(domain_records_url, args[1])
sync_zone(domain_records_url, args[1])
else:
print >>sys.stderr, "[ERROR] Could not find zone file for {0}".format(args[1])
else:
print "You have supplied too many arguments. Usage:"
print "python sync_dns.py will wipe and re-sync all DNS records in the droplet"
print "python sync_dns.py domainname will do an intelligent sync of just that domain"
print "python sync_dns.py domainname --delete will delete the domain record"