From 06410f73fec7dd59779dc3b7849f3ffecaf39af5 Mon Sep 17 00:00:00 2001 From: Adam Sutton Date: Thu, 12 Jul 2012 11:08:01 +0100 Subject: [PATCH 1/4] Re-write of the gandi dyndns script to add some additional options and generally tidying things up. --- gandi-dyndns | 353 ++++++++++++++++++++++++++++++--------------------- 1 file changed, 211 insertions(+), 142 deletions(-) diff --git a/gandi-dyndns b/gandi-dyndns index 9492c6b..a0d65e7 100755 --- a/gandi-dyndns +++ b/gandi-dyndns @@ -1,100 +1,101 @@ -#!/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 = [] - - 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') - optp.add_option('-6', '--ipv6', help='Enable IPv6') - optp.add_option('-r', '--record', help='Specify record data') - (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 - - # Default - if not rtypes: - rtypes = ['A'] - - if check_if_apikey_exists(apikey) == False: - print ("Apikey " + apikey + " does not exist or is malformed") - usage() - sys.exit() - - if check_if_domain_exists(apikey, domain) == False: - print ("Domain " + domain + " does not exist") - usage() - sys.exit() - - addresses = {} - - for rtype in rtypes: - if check_if_record_exists(apikey, get_zoneid_by_domain(apikey, domain), record, rtype) == False: - print (rtype + " Record " + record + " does not exist, please create") - usage() - sys.exit() - - if rtype == 'A': - address = get_public_ipv4() - elif rtype == 'AAAA': - address = get_public_ipv6() - if not address: - print ("Can't find address for record type '" + rtype + "'") - sys.exit() - addresses[rtype] = address - - # Fetch the active zone id - zone_id = get_zoneid_by_domain(apikey, domain) - - # Create a new zone version for zone id - version_id = create_new_zone_version(apikey, zone_id) +#!/usr/bin/env python +# +# gandi-dyndns - Dynamic DNS script for Gandi.net users +# +# Copyright (C) 2012 Adam Sutton +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, version 3 of the License. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +# +# Imports +# + +import os, sys, re +import xmlrpclib, urllib2 +from optparse import OptionParser + +# +# Api wrapper +# +class GandiAPI: + + # Setup + def __init__ ( self, key, domain ): + self._rpc = xmlrpclib.ServerProxy('https://rpc.gandi.net/xmlrpc/') + self._key = key + self._domain = domain + self._zoneid = None + self._zonever = None + + # Check key/domain + vok = False + try: + self.version() + vok = True + di = self.domain_info() + if 'zone_id' in di: + self._zoneid = di['zone_id'] + else: + print 'ERROR: zone id for domain [%s] could not be found' % domain + except Exception, e: + if vok: + print 'ERROR: domain specified [%s] was invalid' % domain + else: + print 'ERROR: apikey specified [%s] was invalid' % key + + # API version + def version ( self ): + return self._rpc.version.info(self._key) + + # Domain info + def domain_info ( self ): + return self._rpc.domain.info(self._key, self._domain) + + # Get zone record + def zone_record_list ( self, name = None, type = None ): + opts = {} + if name: opts['name'] = name + if type: opts['type'] = type + return self._rpc.domain.zone.record.list(self._key, self._zoneid, 0, opts) + + # Get new zone version + def zone_version_new ( self ): + return self._rpc.domain.zone.version.new(self._key, self._zoneid) + + # Delete a zone record + def zone_record_delete ( self, zver, name, type ): + opts = { 'name' : name, 'type' : type } + self._rpc.domain.zone.record.delete(self._key, self._zoneid, zver, opts) + + # Insert a new record + def zone_record_insert ( self, zver, name, type, value, ttl ): + opts = { 'name' : name, 'type' : type, 'value' : value, 'ttl' : ttl } + self._rpc.domain.zone.record.insert(self._key, self._zoneid, zver, opts) - 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]) - 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=APIKEY --domain=DOMAIN --record=RECORD [--ipv4] [--ipv6]") - -def api_version(apikey): - return api.version.info(apikey) - -def zone_list(apikey): - return api.domain.zone.list(apikey) + # Update a record + def zone_record_update ( self, zver, name, type, value, ttl ): + self.zone_record_delete(zver, name, type) + self.zone_record_insert(zver, name, type, value, ttl) -def zone_record_list(apikey, zone_id): - return api.domain.zone.record.list(apikey, zone_id, 0) + # Set active version + def zone_version_set ( self, zver ): + if zver: + self._rpc.domain.zone.version.set(self._key, self._zoneid, zver) -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'] +# +# Get public IP address +# def get_public_ipv4(): try: @@ -103,53 +104,121 @@ def get_public_ipv4(): 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": 300, - "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 - -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 + data = urllib2.urlopen("http://icanhazipv6.com").read() + matches = re.search('

(.*?)

', data) + if matches: + return matches.group(1) + except: pass + return None -if __name__ == "__main__": - main() +# +# Main +# + +if __name__ == '__main__': + + # Process command line + optp = OptionParser() + optp.add_option('-a', '--api', help='Specify API key') + optp.add_option('-d', '--domain', help='Specify domain') + optp.add_option('-r', '--record', help='Specify record data') + optp.add_option('-u', '--update', help='Update existing zone file', + action='store_true') + optp.add_option('-t', '--ttl', help='Specify record TTL [default=300s]', + type='int', default=300) + 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('--test', help='Run test, do not update anything', + action='store_true') + (opts, args) = optp.parse_args() + + # Validate Options + if not opts.api or not opts.domain or not opts.record: + print 'ERROR: you must specify -a -d and -r' + sys.exit(1) + + # Setup api + api = GandiAPI(opts.api, opts.domain) + + # Record types + rtypes = {} + if opts.ipv4: rtypes['A'] = None + if opts.ipv6: rtypes['AAAA'] = None + if not rtypes: + print 'WARN : no record type specified, will default to ipv4' + rtypes = { 'A' : None } + + # Test mode + if opts.test: + print 'INFO : running in test mode, will not update records' + + # Process records + update = {} + for rt in rtypes: + rs = api.zone_record_list(name=opts.record, type=rt) + + # Invalid + if not rs: + print 'WARN : %s record %s does not exist, will skip' % (rt, opts.record) + continue + + # Check address + addr = None + if rt == 'A': + addr = get_public_ipv4() + else: + addr = get_public_ipv6() + if not addr: + print 'WARN : %s could not find public IP address, will skip' % rt + continue + + # Check against record + if rs[0]['value'] == addr: + print 'INFO : %s record already correct, will skip' % rt + continue + + # Update + print 'INFO : %s record will be updated from %s to %s' %\ + (rt, rs[0]['value'], addr) + update[rt] = addr + + # Done + if not update: + print 'INFO : nothing to update' + sys.exit(0) + + # New zone + if not opts.update: + if opts.test: + zver = 12345 + else: + zver = api.zone_version_new() + print 'INFO : created a new zone file version %d' % zver + + # Existing + else: + zver = 0 + print 'INFO : updating the current zone file' + + # Update + for rt in update: + addr = update[rt] + print 'INFO : updating zone %s record %s to %s' % (rt, opts.record, addr) + if not opts.test: + api.zone_record_update(zver, opts.record, rt, addr, opts.ttl) + + # Active new zone file + if not opts.test and zver: + api.zone_version_set(zver) + + # Done + print 'INFO : records updated' + +# ########################################################################### +# Editor +# +# vim:sts=2:ts=2:sw=2:et +# ########################################################################### From cfa17653a59aba03351bcee0928fcfcc002c38f4 Mon Sep 17 00:00:00 2001 From: Adam Sutton Date: Thu, 12 Jul 2012 11:20:18 +0100 Subject: [PATCH 2/4] Cannot actually update current zone (bugger) and typo in record insert. --- gandi-dyndns | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/gandi-dyndns b/gandi-dyndns index a0d65e7..f9c3491 100755 --- a/gandi-dyndns +++ b/gandi-dyndns @@ -81,7 +81,7 @@ class GandiAPI: # Insert a new record def zone_record_insert ( self, zver, name, type, value, ttl ): opts = { 'name' : name, 'type' : type, 'value' : value, 'ttl' : ttl } - self._rpc.domain.zone.record.insert(self._key, self._zoneid, zver, opts) + self._rpc.domain.zone.record.add(self._key, self._zoneid, zver, opts) # Update a record def zone_record_update ( self, zver, name, type, value, ttl ): @@ -123,8 +123,6 @@ if __name__ == '__main__': optp.add_option('-a', '--api', help='Specify API key') optp.add_option('-d', '--domain', help='Specify domain') optp.add_option('-r', '--record', help='Specify record data') - optp.add_option('-u', '--update', help='Update existing zone file', - action='store_true') optp.add_option('-t', '--ttl', help='Specify record TTL [default=300s]', type='int', default=300) optp.add_option('-4', '--ipv4', help='Enable IPv4', @@ -191,17 +189,11 @@ if __name__ == '__main__': sys.exit(0) # New zone - if not opts.update: - if opts.test: - zver = 12345 - else: - zver = api.zone_version_new() - print 'INFO : created a new zone file version %d' % zver - - # Existing + if opts.test: + zver = 12345 else: - zver = 0 - print 'INFO : updating the current zone file' + zver = api.zone_version_new() + print 'INFO : created a new zone file version %d' % zver # Update for rt in update: From fa03d4d067141cfea4ce5f3f83640f050b255344 Mon Sep 17 00:00:00 2001 From: Adam Sutton Date: Thu, 6 Mar 2014 09:18:18 +0000 Subject: [PATCH 3/4] updated IPv4 lookup service. --- gandi-dyndns | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/gandi-dyndns b/gandi-dyndns index f9c3491..5530f37 100755 --- a/gandi-dyndns +++ b/gandi-dyndns @@ -98,10 +98,13 @@ class GandiAPI: # def get_public_ipv4(): + try: + return urllib2.urlopen('http://ipinfo.io/ip').read() + except: pass try: return urllib2.urlopen('http://api.externalip.net/ip/').read() - except: - return None + except: pass + return None def get_public_ipv6(): try: @@ -131,6 +134,7 @@ if __name__ == '__main__': action='store_true') optp.add_option('--test', help='Run test, do not update anything', action='store_true') + optp.add_option('--addrv4', default=None) (opts, args) = optp.parse_args() # Validate Options @@ -166,7 +170,10 @@ if __name__ == '__main__': # Check address addr = None if rt == 'A': - addr = get_public_ipv4() + if opts.addrv4: + addr = opts.addrv4 + else: + addr = get_public_ipv4() else: addr = get_public_ipv6() if not addr: From 940428934345ee596b9cc6d903ff05b57b2c0df0 Mon Sep 17 00:00:00 2001 From: Adam Sutton Date: Tue, 9 Jun 2015 09:23:32 +0100 Subject: [PATCH 4/4] correct bug causing constant zone file updated This ultimately killed my zone! probably because I exceeded the version number limit. Took quite a long time though! --- gandi-dyndns | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gandi-dyndns b/gandi-dyndns index 5530f37..f6b94d6 100755 --- a/gandi-dyndns +++ b/gandi-dyndns @@ -99,10 +99,10 @@ class GandiAPI: def get_public_ipv4(): try: - return urllib2.urlopen('http://ipinfo.io/ip').read() + return urllib2.urlopen('http://ipinfo.io/ip').read().strip() except: pass try: - return urllib2.urlopen('http://api.externalip.net/ip/').read() + return urllib2.urlopen('http://api.externalip.net/ip/').read().strip() except: pass return None @@ -111,7 +111,7 @@ def get_public_ipv6(): data = urllib2.urlopen("http://icanhazipv6.com").read() matches = re.search('

(.*?)

', data) if matches: - return matches.group(1) + return matches.group(1).strip() except: pass return None