-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathgandi-dyndns.py
executable file
·238 lines (214 loc) · 9.53 KB
/
gandi-dyndns.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
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
gandi-dyndns
@author: AlessioCasco
"""
from bottle import route, run, request, response
from optparse import OptionParser
import logging as log
import xmlrpclib
import json
import sys
import re
gandi_fqdn_ip = {}
@route('/ping', method=['GET', 'POST'])
def ping():
'''Function for monitoring/ping'''
response.headers['Server'] = 'gandi-dyndns'
response.status = 200
return('I\'am alive!\n')
@route('/nic_update', method=['GET', 'POST'])
def gandi_dyndns():
'''Main function'''
response.headers['Server'] = 'gandi-dyndns'
# dictionary gandi_fqdn_ip, has fqdn:ip key:value from all the legit requests
global gandi_fqdn_ip
# dictionar ynew_fqdn_ip, has fqdn:ip key:value from the current request
new_fqdn_ip = {}
# define the action to perform into the gandi_api function
action = ''
try:
fqdn, new_ip, fqdn_match = fetch_parameters()
except TypeError:
response.status = 400
return
# create new dictionary with the info we got from the webserver
new_fqdn_ip[fqdn] = new_ip
# check if we need to fetch the ip from gandi
try:
if new_fqdn_ip[fqdn] != gandi_fqdn_ip[fqdn]:
log.debug('Received IP differs from the one saved on Gandi, will update it')
action = 'update'
gandi_fqdn_ip = gandi_api(new_fqdn_ip, gandi_fqdn_ip, fqdn, fqdn_match, action)
return
except KeyError:
log.debug('Do not know the current Gandi IP for fqdn %s, will fetch it' % fqdn)
try:
action = 'fetch'
gandi_fqdn_ip = gandi_api(new_fqdn_ip, gandi_fqdn_ip, fqdn, fqdn_match, action)
if new_fqdn_ip[fqdn] != gandi_fqdn_ip[fqdn]:
action = 'update'
gandi_fqdn_ip = gandi_api(new_fqdn_ip, gandi_fqdn_ip, fqdn, fqdn_match, action)
return
except ValueError:
response.status = 404
return
log.debug('Nothing to do, received IP is same as the one configured on gandi for %s' % fqdn)
return
def fetch_parameters():
'''Fetch parameters from the GET request'''
new_ip = ''
method = request.environ.get('REQUEST_METHOD')
# check for missing parameters
if not request.params.ip and not request.params.fqdn:
log.error('Received malformed request, both parameters (fqdn & ip) are missing. Got: \"%s\"' % request.url)
return
elif not request.params.ip:
new_ip = request.environ.get('REMOTE_ADDR')
log.debug('IP parameter is missing, will use client source one: %s' % new_ip)
elif not request.params.fqdn:
log.error('Received malformed request, fqdn parameter is missing. Got: \"%s\"' % request.url)
return
if not new_ip:
new_ip = request.params.ip
fqdn = request.params.fqdn
# check if parameters have correct informations
fqdn_match = re.match(r'^([a-zA-Z0-9][a-zA-Z0-9-]{1,61})\.([a-zA-Z0-9][a-zA-Z0-9-]{1,61}\.[a-zA-Z]{2,}$)', fqdn)
ip_match = re.match(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$', new_ip)
priv_ip_match = re.match(r'^(?:10|127|172\.(?:1[6-9]|2[0-9]|3[01])|192\.168)\..*', new_ip)
if not fqdn_match and not ip_match:
log.error('Received invalid values on both parameters. Got fqdn:\"%s\" & IP: %s' % (fqdn, new_ip))
return
elif not ip_match:
log.error('Received invalid ip value. Got %s' % new_ip)
return
elif priv_ip_match:
log.error('Received IP is not a public one. Got %s' % new_ip)
return
elif not fqdn_match:
log.error('Received invalid fqdn value. Got \"%s\"' % fqdn)
return
log.debug('Received %s request: fqdn:\"%s\" & IP: %s' % (method, fqdn, new_ip))
return fqdn, new_ip, fqdn_match
def gandi_api(new_fqdn_ip, gandi_fqdn_ip, fqdn, fqdn_match, action):
'''Funcion for managing the Gandi API'''
# define some variables about gandi
api = xmlrpclib.ServerProxy('https://rpc.gandi.net/xmlrpc/')
apikey = config['apikey']
hostname = (fqdn_match.group(1))
domain = (fqdn_match.group(2))
# check if the domain is managed by the apikey provided
if not (api.domain.list(apikey, {'~fqdn': domain})):
log.error('Apikey provided does not manage %s domain' % domain)
raise ValueError('Apikey provided does not manage %s domain' % domain)
# check available zones
zones = api.domain.zone.list(apikey)
for zone in zones:
if (zone['name']) == domain:
zone_id = zone['id']
log.debug('Zone id %s found, for domain %s' % (zone_id, domain))
break
else:
log.error('Could not find zone file called %s, you must have a zone having same name as the domain you want to manage' % domain)
raise ValueError('Could not find zone file called %s, you must have a zone having same name as the domain you want to manage' % domain)
# check if we have to fetch the gandi api
if action == 'fetch':
# check & retrieve informations from recods in zone
records = api.domain.zone.record.list(apikey, zone_id, 0)
for record in records:
if (record['name'] == hostname and record['type'].lower() == 'a'):
# add fqdn/ip to the gandi_fqdn_ip dictionary
gandi_fqdn_ip[fqdn] = record['value']
log.debug('DNS \'A\' record found for subdomain \'%s\' having value %s' % (hostname, gandi_fqdn_ip[fqdn]))
break
else:
log.error('Unable to find a DNS \'A\' record for subdomain \'%s\'' % hostname)
raise ValueError('Unable to find a DNS \'A\' record for subdomain \'%s\'' % hostname)
return gandi_fqdn_ip
# check if we have to update the the ip
elif action == 'update':
# create a new zone from the existing one
zone_version = api.domain.zone.version.new(apikey, zone_id)
log.debug('New zone created, new version: %s' % zone_version)
# delete the A record from the new version
api.domain.zone.record.delete(apikey, zone_id, zone_version, {"type": ["A"], "name": [hostname]})
log.debug('Deleted \'A\' record from new zone version %s' % zone_version)
# add the A record we want
new_record = api.domain.zone.record.add(apikey, zone_id, zone_version, {"type": "A", "name": hostname, "value": new_fqdn_ip[fqdn], "ttl": 300})
log.debug('New \'A\' record added as follow: %s' % new_record)
# active the new zone version
if api.domain.zone.version.set(apikey, zone_id, zone_version):
log.info('New IP %s for fqdn %s updated succesfully.' % (new_fqdn_ip[fqdn], fqdn))
else:
log.error('Unable to update IP %s for fqdn %s' % (new_fqdn_ip[fqdn], fqdn))
return
# update gandi_fqdn_ip with the value just saved in the new zone version
gandi_fqdn_ip[fqdn] = new_fqdn_ip[fqdn]
return gandi_fqdn_ip
def init_application():
def get_options():
'''Load options from the command line'''
default_config = "config.json"
parser = OptionParser(usage="usage: %prog [options]")
parser.add_option(
"-c",
"--config",
dest="configfile",
default=default_config,
help='Config file relative or absolute path. Default is %s' % default_config)
(options, args) = parser.parse_args()
if options.configfile is not None:
options.configfile = options.configfile.strip(' ')
return options
def read_config_file(configfile):
'''Loads the config file from disk'''
try:
with open(configfile) as f:
config = validate_config(json.load(f))
return config
# catch if file doesn't exist
except IOError:
print('Config file %s not found' % configfile)
sys.exit(1)
# catch if json file is not formatted corectly
except ValueError:
print('Json file is not formatted properly')
sys.exit(1)
def validate_config(raw_config):
'''Checks the config file.'''
# check if required patameters are present inside the config
if 'port' not in raw_config or 'bind' not in raw_config or 'apikey' not in raw_config or 'logging' not in raw_config:
print('Config file has missing parameters')
sys.exit(1)
else:
return raw_config
def configure_logging(config):
'''Configure logging'''
if config['logging']['log_enable'] == "false":
log.disable('CRITICAL')
return
elif config['logging']['log_enable'] == "true":
try:
log.basicConfig(
format='%(asctime)-15s [%(levelname)s] %(message)s',
filename=config['logging']['log_file'],
level=config['logging']['log_level'])
except ValueError:
print('Log level is not set with a correct value, check the README.md for the full list')
sys.exit(1)
except IOError:
print('Unable to create the log file, check if gandi-dyndns has write permissions')
sys.exit(1)
return
else:
print('Bad congig file, log_enable is not set with a correct value, (true|false) are the two only options')
sys.exit(1)
options = get_options()
config = read_config_file(options.configfile)
configure_logging(config)
return config
if __name__ == "__main__":
config = init_application()
# init webserver
run(host=config["bind"], port=config["port"], quiet=True)