-
Notifications
You must be signed in to change notification settings - Fork 0
/
nomad.py
executable file
·397 lines (342 loc) · 13.1 KB
/
nomad.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
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
#!/usr/bin/python
import requests
import sys, os, time
import getopt
version = '1.0.0'
author = 'Chris Martin'
date = 'March 2017'
# Parse the command-line options
clopt = getopt.getopt(sys.argv[1:],
'upcCD:hvdfL',
['help', 'verbose', 'debug', 'force', 'remove',
'domain=', 'user=', 'passwd=', 'config=',
'cachedir=', 'timefmt='])
# Transform the output of getopt() into a dictionary
clopt_flags = {x[0]:x[1] for x in clopt[0]}
if '-h' in clopt_flags or '--help' in clopt_flags:
sys.stdout.write("""
NOMAD dynamic DNS update service for ZoneEdit
v""" + version + """
Checks the system's current IP address with a call to ipinfo.io and
compares it against the last known value. If there has been a change,
nomad issues DDNS update request to the ZoneEdit server.
http://forum.zoneedit.com/index.php?threads/setting-up-dynamic-dns-for-your-domain.4/
HELP
-h or --help
Print this help text.
ZONEEDIT LOGIN OPTIONS
-u or --user
Sets the username used to log in to ZoneEdit
-uUSERNAME or --user USERNAME
Set by the *user* directive in the config file
user = 'USERNAME'
-p or --passwd
Sets the plaintext password or token used to log in to ZoneEdit.
Your administrative password will work, but you should use a login
token for security.
-pTOKEN or --passwd TOKEN
Set by the *passwd* directive in the config file
passwd = 'TOKEN'
CONFIGURING THE BEHAVIOR OF NOMAD
-f or --force
If this flag is found, then a DNS update will be forced regardless
of the result of the comparison against the cached ip file.
Set by the *force* directive in the config file
force = True or force = False
-c or --config
Sets the nomad configuration file
-c/etc/nomad_config.py or --config /etc/nomad_config.py
Set by the *config* directive in the config file
config = '/etc/nomad_config.py'
-C or --cachedir
Sets the directory where the ip and log files are stored. nomad
needs rwx permissions here. Ending / is not mandatory.
-d/var/cache/nomad/ or --cachedir /var/cache/nomad/
Set by the *cachedir* directive in the config file
cachedir = '/var/cache/nomad'
-D or --domain
Sets the domain whose IP should be updated.
-Dmydomain.com --domain mydomain.com
Set by the *domain* directive in the config file
domain = 'mydomain.com'
--timefmt
Sets the time format string used when logging. The format string is
passed verbatim to the python time.strftime() function to generate
the time stamp.
--timefmt '[%D %T]'
Set by the *timefmt* directive in the config file
timefmt = '[%D %T]'
--update
Update the poling interval in crontab and exit without performing any
of the usual checks or actions.
DEBUGGING OPTIONS
Debugging should usually be done using the -dv options together.
-d or --debug
Changes the behavior of nomad to perform a dry-run. It will load the
configuration, check the IP address, but it will not actually connect to
the ZoneEdit servers.
-v or --verbose
Prints the behavior of nomad to the screen for debugging.
-L
Log more heavily than usual. By default, only errors that need
attention or DNS updates will be logged. When the -L flag is set
the result of each IP check will be logged. This can be useful to
verify that the regular checks are happening correctly.
Set by the *heavylog* directive in the config file
heavylog = True
(c) 2017 Christopher R. Martin
""")
exit(0)
# Configuration defaults
config_dict = {
'domain':None,
'user':None,
'passwd':None,
'cachedir':'/var/cache/nomad',
'timefmt':'[%D %T]',
'config':'/etc/nomad.conf',
'heavylog':False,
'debug':False,
'force':False,
'verbose':False
}
# Define a routine for setting config parameters from the short and long
# options
# dopt Indicates the config_dict keyword to modify
# opt Is a list of equivalent option strings as they appear in the output
# of getopt(). For example, ['-o', '--output'] might indicate two
# equivalent flags that should both be mapped to the same item in the
# configuration dictionary.
# The clopt_flags dict will be checked for these options. When any one of the
# options in the opt list is found in the clopt output, its value will be
# written to config_dict[].
#
# set_config('user', ['-u', '--user'])
#
# will place the argument of the '-u' and '--user' flags into
# config_dict['user'].
def set_config(dopt, opt=[]):
found = None
for thisopt in opt:
if thisopt in clopt_flags:
if found is None:
found = thisopt
else:
sys.stderr.write(
'Redundant command line options %s, %s'%(found, thisopt))
exit(-1)
if found is not None:
if clopt_flags[found] == '':
config_dict[dopt] = True
else:
config_dict[dopt] = clopt_flags[found]
# First, assign the config file if it is present in the CL options.
set_config('config',['-c','--config'])
# Go get the configuration
# Execute the config file with the configuration dictionary as the locals dict
# Read/write privileges to the configuration file should be carefully
# controlled since it will contain the access token for the DDNS service and
# the contents will be executed verbatim.
config_file = config_dict['config']
if os.access(config_file, os.R_OK):
try:
with open(config_file,'r') as cf:
exec(cf.read(),config_dict)
except:
sys.stderr.write('Invalid configuration file.\n')
sys.stderr.write(sys.exc_info()[1].message)
exit(-1)
else:
sys.stderr.write('Could not open configuration file: ' + config_file)
exit(-1)
# Now read in the rest of the command line arguments. Putting them second
# allows them to override the config file.
set_config('domain',['-D','--domain'])
set_config('verbose',['-v','--verbose'])
set_config('debug',['-d','--debug'])
set_config('force',['-f','--force'])
set_config('user',['-u','--user'])
set_config('passwd',['-p','--passwd'])
set_config('cachedir',['-C','--cachedir'])
set_config('timefmt',['--timefmt'])
set_config('heavylog',['-L'])
# Build the log and IP file names
cache_dir = config_dict['cachedir']
log_file = os.path.join(config_dict['cachedir'],'log')
ip_file = os.path.join(config_dict['cachedir'],'ip')
verbose = bool(config_dict['verbose'])
debug = bool(config_dict['debug'])
force = bool(config_dict['force'])
heavylog = bool(config_dict['heavylog'])
timefmt = config_dict['timefmt']
update = '--update' in clopt_flags
# Before we go mucking about with files, be verbose to help with any debugging
# in case something breaks accessing the cache files
if verbose:
sys.stdout.write(
"""*** NOMAD ***
Version %s
By %s, %s
Using configuration file: %s
Using log file: %s
Using ip file: %s
Domain: %s
User: %s
Force update: %s
*** ***
"""%(version, author, date, config_file, log_file, ip_file,
config_dict['domain'], config_dict['user'], str(config_dict['force'])))
cache_ip = (-1,-1,-1,-1)
web_ip = (-1,-1,-1,-1)
# Check the log file. Failure to write to the log file is lethal.
if not os.access(log_file, os.R_OK):
sys.stderr.write('Failed to open the log file: %s\n'%log_file)
exit(-1)
with open(log_file, 'a') as logf:
# Attempt to get the system's current ip address from ipinfo.io
IP_WEB_FAIL = False
IP_WEB_MEESSAGE = ''
if verbose:
sys.stdout.write("Checking current ip...")
try:
# Issue the web request. Failure raises an exception caught by try
page = requests.get('http://ipinfo.io')
# Extract the IP address
iptext = page.json()['ip']
except:
IP_WEB_FAIL = True
IP_WEB_MESSAGE = sys.exc_info()[1].message
# If everything is going well, then convert the text into a tuple of integers
if not IP_WEB_FAIL and page.status_code >= 200 and page.status_code <= 299:
try:
web_ip = tuple([int(this) for this in iptext.split('.')])
if len(web_ip)!=4:
raise Exception()
except:
IP_WEB_FAIL = True
IP_WEB_MESSAGE = "Found nonsense IP address: %s"%iptext
# If the status code wasn't 200
elif not IP_WEB_FAIL:
IP_WEB_FAIL = True
IP_WEB_MESSAGE = ip_page.content
if verbose:
if IP_WEB_FAIL:
sys.stdout.write("[FAILED]\n")
else:
sys.stdout.write(iptext + "\n")
# Log a web lookup failure
if IP_WEB_FAIL:
logf.write(time.strftime(timefmt) + 'Failed to obtain IP address\n')
logf.write(IP_WEB_MESSAGE + '\n')
elif heavylog:
logf.write(time.strftime(timefmt) + 'Current IP address %s\n'%iptext)
# Get the Former IP address from the cached IP file
IP_CACHE_FAIL = False
IP_CACHE_MESSAGE = ''
IP_CHANGE = False
if verbose:
sys.stdout.write("Checking cached IP address...")
# Does the ip cache file exist?
if os.access(ip_file,os.F_OK):
try:
# Read the old IP address and convert it into a tuple
with open(ip_file,'r') as ff:
iptext = ff.read()
cache_ip = tuple([int(this) for this in iptext.split('.')])
except:
IP_CACHE_FAIL = True
IP_CACHE_MESSAGE = 'Failed to open file %s'%ip_file
# Compare the new IP to the old IP
if not (IP_WEB_FAIL or IP_CACHE_FAIL):
IP_CHANGE = cache_ip != web_ip
if heavylog and not IP_CACHE_FAIL:
logf.write(time.strftime(timefmt) + 'Cached IP address %s\n'%iptext)
# If there is no IP history, then default to a change
else:
IP_CHANGE = True
INTERVAL_CHANGE = True
logf.write(time.strftime(timefmt) + 'IP cache not found: %s\n'%ip_file)
if verbose:
if IP_CACHE_FAIL:
sys.stdout.write("[FAILED]\n")
else:
sys.stdout.write(iptext + '\n')
if IP_CHANGE:
sys.stdout.write("IP Changed.\n")
# Update the cache IP address
IP_UPDATE_FAIL = False
IP_UPDATE_MESSAGE = ''
# Update the IP file ONLY if there is something meaningful to put in its place
if IP_CHANGE and not IP_WEB_FAIL:
if verbose:
sys.stdout.write("Updating IP cache...")
try:
with open(ip_file,'w') as ff:
ff.write('%d.%d.%d.%d'%web_ip)
cmd = 'chmod 644 %s'%ip_file
os.system(cmd)
except:
IP_UPDATE_FAIL = True
IP_UPDATE_MESSAGE = 'Failed to write IP cache: %s'%ip_file
if verbose:
if IP_UPDATE_FAIL:
sys.stdout.write("[FAILED]\n")
else:
sys.stdout.write("[done]\n")
# A failure to update the IP cache is fatal. Log it and exit with an error.
if IP_UPDATE_FAIL:
logf.write(time.strftime(timefmt) +
IP_UPDATE_MESSAGE)
sys.stderr.write(IP_UPDATE_MESSAGE)
exit(-1)
# Update the DDNS
DNS_UPDATE_FAIL = False
DNS_UPDATE_MESSAGE = ''
# Update the DDNS if
# (1) there has been a change (IP_CHANGE is True)
# This prevents nomad from banging away on the ZoneEdit DDNS service
# pointlessly.
# (2) the cache update succeeded (IP_UPDATE_FAIL is False)
# This prevents a file write permission error from fooling nomad into
# believing that there has been a change in IP every time it checks.
# (3) the debug flag was not set
if (force or (IP_CHANGE and not IP_UPDATE_FAIL)) and not debug:
if verbose:
if force:
sys.stdout.write('Forcing an update to the ZoneEdit NDS Record...')
else:
sys.stdout.write("Updating the ZoneEdit DNS record...")
host = config_dict['domain']
ip = '%d.%d.%d.%d'%web_ip
url = 'https://dynamic.zoneedit.com/auth/dynamic.html' + \
'?host=%s&dnsto=%s'%(host,ip)
try:
page = requests.get( url + '?host=%s&dnsto=%s'%(host,ip),
auth = (config_dict['user'], config_dict['passwd']))
except:
DNS_UPDATE_FAIL = True
DNS_UPDATE_MESSAGE = sys.exc_info()[1].message
# If communication was successful, test the server response
if not DNS_UPDATE_FAIL:
DNS_UPDATE_MESSAGE = page.content
if page.status_code < 200 or page.status_code > 299:
DNS_UPDATE_FAIL = True
elif 'SUCCESS' not in page.content:
DNS_UPDATE_FAIL = True
if verbose:
if DNS_UPDATE_FAIL:
sys.stdout.write("[FAILED]\n")
else:
sys.stdout.write("[success]\n")
if DNS_UPDATE_FAIL:
logf.write(time.strftime(timefmt) +
'--> DNS UPDATE FAILURE!\n%s\n'%DNS_UPDATE_MESSAGE)
elif force:
logf.write(time.strftime(timefmt) +
'Forced a DNS update.\n%s\n'%DNS_UPDATE_MESSAGE)
else:
logf.write(time.strftime(timefmt) +
'Updated DNS record.\n%s\n'%DNS_UPDATE_MESSAGE)
# The password is no longer needed. Delete it.
del config_dict['passwd']
exit(0)