diff --git a/README b/README deleted file mode 100644 index 36b36e3..0000000 --- a/README +++ /dev/null @@ -1,25 +0,0 @@ ---- Notice --- - -The development of this tool or myself are in no way involved with Gandi.net. - ---- Requirements --- - -- Python (>= 2.7) -- Python xmlrpclib - ---- Gandi details --- - -API Key (Apply here: https://www.gandi.net/admin/api_key) - ---- Usage --- - -gandi-dyndns --api=APIKEY --domain=DOMAIN --record=RECORD - ---- Readme --- - -Currently very lazy but working mode. -Note that there is a gandi bug, as where the program updates the zone and activates it, gandi does not show this through their webinterface. -I'll be contacting them to fix this. - -All communication is done over https. - diff --git a/README.md b/README.md new file mode 100644 index 0000000..29cd076 --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +# Gandi Dyndns v5 + +Automatically update your IPv4 and/or IPv6 DNS records using [Gandi](https://www.gandi.net/)'s [LiveDNS](https://api.gandi.net/docs/livedns/) API. + +This project is a fork of https://github.com/lembregtse/gandi-dyndns mostly rewritten to work with Python 3 and the newer API v5 instead of API v4. Public IP is retrieved through [icanhaz](https://github.com/major/icanhaz). + +The development of this tool or myself are in no way involved with Gandi.net + +# Requirements + +Everything should come with a default Python setup: + +* Python 3 +* re +* sys +* requests +* optparse + +# API Key + +Generate your API key from Gandi's [API Key Page](https://www.gandi.net/admin/api_key) in the Security section. + +# Usage + +``` +Usage: gandi-dyndns --api= --domain= --record= [--ipv4] [--ipv6] [--quiet] +Example: gandi-dyndns --api=123ApIkEyFrOmGanDi --domain=example.com --record=www --ipv4 +``` \ No newline at end of file diff --git a/TODO b/TODO deleted file mode 100644 index 672a032..0000000 --- a/TODO +++ /dev/null @@ -1,5 +0,0 @@ -- add init.d method? -- cleanup the code -- ask Gandi why version.set does not update the gui directly (takes 1 hour) -- add local history for IP to check weither we need to use gandi rpc -- add checks and warnings / errors diff --git a/gandi-dyndns b/gandi-dyndns deleted file mode 100755 index 84f2c26..0000000 --- a/gandi-dyndns +++ /dev/null @@ -1,209 +0,0 @@ -#!/usr/bin/python - -import re -import xmlrpclib -import getopt -import sys -import urllib2 - -api = xmlrpclib.ServerProxy('https://rpc.gandi.net/xmlrpc/') - -def main(): - apikey = '' - domain = '' - record = '' - rtypes = [] - quiet=False - - from optparse import OptionParser - optp = OptionParser() - optp.add_option('-a', '--api', help='Specify API key') - optp.add_option('-d', '--domain', help='Specify domain') - optp.add_option('-4', '--ipv4', help='Enable IPv4', action='store_true') - optp.add_option('-6', '--ipv6', help='Enable IPv6', action='store_true') - optp.add_option('-r', '--record', help='Specify record data') - optp.add_option('--extip4', help='Force external IPv4. This can be used to update a record with an IP different than the IP of the server/workstation from which the script is executed') - optp.add_option('--extip6', help='Force external IPv6. This can be used to update a record with an IP different than the IP of the server/workstation from which the script is executed') - optp.add_option('-q', '--quiet', help='No output except to stderr on error', action='store_true') - (opts, args) = optp.parse_args() - - # Process - if opts.ipv4: rtypes.append('A') - if opts.ipv6: rtypes.append('AAAA') - domain = opts.domain - apikey = opts.api - record = opts.record - extip4 = opts.extip4 - extip6 = opts.extip6 - if opts.quiet: quiet=True - - if apikey == None or apikey == '': - print >> sys.stderr, ("No Apikey specified") - usage() - sys.exit(79) - - #This call will always return true. cf comment regarding check_if_apikey_exists - if check_if_apikey_exists(apikey) == False: - print >> sys.stderr, ("Apikey " + apikey + " does not exist or is malformed") - usage() - sys.exit(80) - - if domain == None: - print >> sys.stderr, ("No Domain specified") - usage() - sys.exit(81) - - if check_if_domain_exists(apikey, domain) == False: - print >> sys.stderr, ("Domain " + domain + " does not exist") - usage() - sys.exit(82) - - # Default - if not rtypes: - rtypes = ['A'] - - addresses = {} - - for rtype in rtypes: - if check_if_record_exists(apikey, get_zoneid_by_domain(apikey, domain), record, rtype) == False: - print >> sys.stderr, (rtype + " Record " + record + " does not exist, please create") - usage() - sys.exit(83) - - if rtype == 'A': - if extip4 != "" and not extip4 == None: - address = extip4 - else : - address = get_public_ipv4() - elif rtype == 'AAAA': - if extip6 != "" and not extip6 == None: - address = extip6 - else : - address = get_public_ipv6() - if not address: - print >> sys.stderr, ("Can't find address for record type '" + rtype + "'") - sys.exit(84) - addresses[rtype] = address - - # Fetch the active zone id - zone_id = get_zoneid_by_domain(apikey, domain) - - # Check if the current IPv4 address changed - for rtype in rtypes: - ip_changed = check_if_ip_changed(apikey, zone_id, record, rtype, addresses[rtype]) - if not ip_changed: - if not quiet: - print ("The IP for '" + record + "' did not change") - sys.exit(0) - - # Create a new zone version for zone id - version_id = create_new_zone_version(apikey, zone_id) - - for rtype in rtypes: - # Update the record for the zone id and zone version - update_record(apikey, zone_id, version_id, record, rtype, addresses[rtype]) - if not quiet: - print record + " " + rtype + " " + addresses[rtype] - - # Activate the new zone - api.domain.zone.version.set(apikey, zone_id, version_id) - -def usage(): - print("Usage: gandi-dyndns --api= --domain= --record= [--ipv4] [--ipv6] [--quiet]") - print("Example: gandi-dyndns --api=123ApIkEyFrOmGanDi --domain=example.com --record=www --ipv4") - -def api_version(apikey): - return api.version.info(apikey) - -def zone_list(apikey): - return api.domain.zone.list(apikey) - -def zone_record_list(apikey, zone_id): - return api.domain.zone.record.list(apikey, zone_id, 0) - -def create_new_zone_version(apikey, zone_id): - return api.domain.zone.version.new(apikey, zone_id) - -def domain_info(apikey, domain): - return api.domain.info(apikey, domain) - -def get_zoneid_by_domain(apikey, domain): - return domain_info(apikey, domain)['zone_id'] - -def get_public_ipv4(): - try: - data= urllib2.urlopen('http://checkip.dyndns.com/').read() - matchip = re.search('Current IP Address: (.*?)', data) - if matchip: - return matchip.group(1) - except: pass - return None - -def get_public_ipv6(): - data = urllib2.urlopen("http://icanhazipv6.com").read() - matches = re.search('

(.*?)

', data) - if matches: - return matches.group(1) - return None - -def update_record(apikey, zone_id, zone_version, record, rtype, value): - delete_record(apikey, zone_id, zone_version, record, rtype) - insert_record(apikey, zone_id, zone_version, record, rtype, value) - -def delete_record(apikey, zone_id, zone_version, record, rtype): - recordListOptions = {"name": record, - "type": rtype} - - records = api.domain.zone.record.delete(apikey, zone_id, zone_version, recordListOptions) - -def insert_record(apikey, zone_id, zone_version, record, rtype, value): - zoneRecord = {"name": record, - "ttl": 10800, - "type": rtype, - "value": value} - - api.domain.zone.record.add(apikey, zone_id, zone_version, zoneRecord) - -def check_if_domain_exists(apikey, domain): - try: - api.domain.info(apikey, domain) - return True - except xmlrpclib.Fault as err: - return False - -#will always return true, even with malformed or empty apikey. -#api.version.info(apikey) in api_version returns a valid API version -#even if apikey is empty string or None. -def check_if_apikey_exists(apikey): - try: - api_version(apikey) - return True - except xmlrpclib.Fault as err: - return False - -def check_if_record_exists(apikey, zone_id, record, rtype): - recordListOptions = {"name": record, - "type": rtype} - - records = api.domain.zone.record.list(apikey, zone_id, 0, recordListOptions) - if len(records) > 0: - return True - - return False - -def check_if_ip_changed(apikey, zone_id, record, rtype, external_ip): - recordListOptions = {"name": record, - "type": rtype} - - records = api.domain.zone.record.list(apikey, zone_id, 0, recordListOptions) - if len(records) > 0: - for record in records: - if record['value'] == external_ip: - return False - - return True - - return False - -if __name__ == "__main__": - main() diff --git a/gandi-dyndns.py b/gandi-dyndns.py new file mode 100644 index 0000000..a0950aa --- /dev/null +++ b/gandi-dyndns.py @@ -0,0 +1,136 @@ +#!/usr/bin/python3 + +import re +import sys +import requests + +from optparse import OptionParser + +GANDI_API = 'https://api.gandi.net/v5/' + +def api_request(apikey, endpoint, payload=None): + if payload is not None: + result = requests.put(GANDI_API + endpoint, json=payload, headers={"Authorization": "Apikey " + apikey}) + else: + result = requests.get(GANDI_API + endpoint, headers={"Authorization": "Apikey " + apikey}) + result.raise_for_status() + return result.json() + +def list_domains(apikey): + return api_request(apikey, 'livedns/domains') + +def get_record(apikey, domain, name, rtype): + return api_request(apikey, 'livedns/domains/' + domain + '/records/' + name + '/' + rtype) + +def update_record(apikey, domain, name, rtype, payload): + return api_request(apikey, 'livedns/domains/' + domain + '/records/' + name + '/' + rtype, payload) + +def get_public_ipv4(): + result = requests.get('https://ipv4.icanhazip.com/') + result.raise_for_status() + matchip = re.search('([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+)', result.text) + if matchip: + return matchip.group(1) + return None + +def get_public_ipv6(): + result = requests.get('https://ipv6.icanhazip.com/') + result.raise_for_status() + matchip = re.search('([0-9a-f]+:[0-9a-f]+:[0-9a-f]+:[0-9a-f]+:[0-9a-f]+:[0-9a-f]+:[0-9a-f]+:[0-9a-f]+)', result.text) + if matchip: + return matchip.group(1) + return None + +def usage(): + print('Usage: gandi-dyndns --api= --domain= --record= [--ipv4] [--ipv6] [--quiet]') + print('Example: gandi-dyndns --api=123ApIkEyFrOmGanDi --domain=example.com --record=www --ipv4') + +def main(): + apikey = '' + domain = '' + record = '' + rtypes = [] + quiet=False + + optp = OptionParser() + optp.add_option('-a', '--api', help='Specify API key') + optp.add_option('-d', '--domain', help='Specify domain') + optp.add_option('-4', '--ipv4', help='Enable IPv4', action='store_true') + optp.add_option('-6', '--ipv6', help='Enable IPv6', action='store_true') + optp.add_option('-r', '--record', help='Specify record data') + optp.add_option('--extip4', help='Force external IPv4. This can be used to update a record with an IP different than the IP of the server/workstation from which the script is executed') + optp.add_option('--extip6', help='Force external IPv6. This can be used to update a record with an IP different than the IP of the server/workstation from which the script is executed') + optp.add_option('-q', '--quiet', help='No output except to stderr on error', action='store_true') + (opts, args) = optp.parse_args() + + # Process Arguments + if opts.ipv4: rtypes.append('A') + if opts.ipv6: rtypes.append('AAAA') + domain = opts.domain + apikey = opts.api + record = opts.record + extip4 = opts.extip4 + extip6 = opts.extip6 + if opts.quiet: quiet=True + if not rtypes: rtypes = ['A'] + + if apikey == None or apikey == '': + print('No Apikey specified', file=sys.stderr) + usage() + sys.exit(79) + + if domain == None: + print('No Domain specified', file=sys.stderr) + usage() + sys.exit(81) + + try: + domain_list = list_domains(apikey) + except requests.exceptions.HTTPError as err: + print('Failed to validate API key: ' + str(err)) + usage() + sys.exit(80) + + domain_found = False + for item in domain_list: + if 'fqdn' in item and item['fqdn'] == domain: + domain_found = True + break + + if not domain_found: + print('Domain ' + domain + ' does not exist on provided API account', file=sys.stderr) + usage() + sys.exit(82) + + for rtype in rtypes: + if rtype == 'A': + public_ip = get_public_ipv4() + if rtype == 'AAAA': + public_ip = get_public_ipv6() + if not public_ip: + print('Failed to determine public IP address of type ' + rtype, file=sys.stderr) + sys.exit(84) + + try: + record_data = get_record(apikey, domain, record, rtype) + except requests.exceptions.HTTPError as err: + print('Failed to retrieve ' + rtype + ' record "' + record + '" for domain "' + domain + '": ' + str(err), file=sys.stderr) + usage() + sys.exit(83) + + if public_ip in record_data['rrset_values']: + if not quiet: + print('The public IP address for ' + rtype + ' record did not change: ' + public_ip) + else: + if not quiet: + print('Updating "' + record + '" ' + rtype + ' record: ' + record_data['rrset_values'][0] + ' -> ' + public_ip) + record_data['rrset_values'] = [ public_ip ] + try: + update_record(apikey, domain, record, rtype, record_data) + except requests.exceptions.HTTPError as err: + print('Failed to update record of type ' + rtype + ': ' + str(err), file=sys.stderr) + sys.exit(85) + +if __name__ == "__main__": + main() +