diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5ae70aa --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +/.idea/ +/src/build/ +*.log +*.pyc diff --git a/src/gandyn.py b/src/gandyn.py index af89e1d..7020923 100755 --- a/src/gandyn.py +++ b/src/gandyn.py @@ -12,12 +12,14 @@ DOMAIN_NAME = 'mydomain.com' TTL = 300 -RECORD = {'type':'A', 'name':'@'} +RECORD = {'type': 'A', 'name': '@'} LOG_LEVEL = logging.INFO LOG_FILE = 'gandyn.log' -class GandiDomainUpdater( object ): +IP_TRY_COUNT = 5 + +class GandiDomainUpdater(object): """Updates a gandi DNS record value.""" def __init__(self, api_key, domain_name, record): """Constructor @@ -33,26 +35,26 @@ def __init__(self, api_key, domain_name, record): self.__api = xmlrpc.client.ServerProxy('https://rpc.gandi.net/xmlrpc/') self.__zone_id = None - def __get_active_zone_id( self ): + def __get_active_zone_id(self): """Retrieve the domain active zone id.""" - if self.__zone_id == None : - self.__zone_id = self.__api.domain.info( - self.api_key, - self.domain_name - )['zone_id'] + if self.__zone_id is None: + self.__zone_id = self.__api.domain.zone.list( + self.api_key, + {"name":self.domain_name} + )[0]['id'] return self.__zone_id - def get_record_value( self ): + def get_record_value(self): """Retrieve current value for the record to update.""" zone_id = self.__get_active_zone_id() return self.__api.domain.zone.record.list( - self.api_key, - zone_id, - 0, - self.record - )[0]['value'] + self.api_key, + zone_id, + 0, + self.record + )[0]['value'] - def update_record_value( self, new_value, ttl=300 ): + def update_record_value(self, new_value, ttl=300): """Updates record value. Update is done on a new zone version. If an error occurs, @@ -62,64 +64,66 @@ def update_record_value( self, new_value, ttl=300 ): new_zone_version = None zone_id = self.__get_active_zone_id() try: - #create new zone version + # create new zone version new_zone_version = self.__api.domain.zone.version.new( - self.api_key, - zone_id - ) + self.api_key, + zone_id + ) logging.debug('DNS working on a new zone (version %s)', new_zone_version) record_list = self.__api.domain.zone.record.list( - self.api_key, - zone_id, - new_zone_version, - self.record - ) - #Update each record that matches the filter + self.api_key, + zone_id, + new_zone_version, + self.record + ) + # Update each record that matches the filter for a_record in record_list: - #get record id + # get record id a_record_id = a_record['id'] a_record_name = a_record['name'] a_record_type = a_record['type'] - #update record value + # update record value new_record = self.record.copy() new_record.update({'name': a_record_name, 'type': a_record_type, 'value': new_value, 'ttl': ttl}) - updated_record = self.__api.domain.zone.record.update( - self.api_key, - zone_id, - new_zone_version, - {'id': a_record_id}, - new_record - ) - except xmlrpc.client.Fault as e: - #delete updated zone - if new_zone_version != None : + self.__api.domain.zone.record.update( + self.api_key, + zone_id, + new_zone_version, + {'id': a_record_id}, + new_record + ) + except xmlrpc.client.Fault: + # delete updated zone + if new_zone_version is not None: self.__api.domain.zone.version.delete( - self.api_key, - zone_id, - new_zone_version - ) + self.api_key, + zone_id, + new_zone_version + ) raise else: - #activate updated zone + # activate updated zone self.__api.domain.zone.version.set( - self.api_key, - zone_id, - new_zone_version - ) + self.api_key, + zone_id, + new_zone_version + ) + def usage(argv): - print(argv[0],' [[-c | --config] ] [-h | --help]') + print(argv[0], ' [[-c | --config] ] [-h | --help]') print('\t-c --config : Path to the config file') print('\t-h --help : Displays this text') + def main(argv, global_vars, local_vars): try: options, remainder = getopt.getopt(argv[1:], 'c:h', ['config=', 'help']) for opt, arg in options: if opt in ('-c', '--config'): config_file = arg - #load config file + # load config file exec( compile(open(config_file).read(), config_file, 'exec'), global_vars, @@ -134,21 +138,23 @@ def main(argv, global_vars, local_vars): exit(1) try: - logging.basicConfig(format='%(asctime)s %(levelname)-8s %(message)s', datefmt='%a, %d %b %Y %H:%M:%S', level=LOG_LEVEL, filename=LOG_FILE) - public_ip_retriever = ipretriever.adapter.IPEcho() + logging.basicConfig( + format='%(asctime)s %(levelname)-8s %(message)s', + datefmt='%a, %d %b %Y %H:%M:%S', + level=LOG_LEVEL, + filename=LOG_FILE) gandi_updater = GandiDomainUpdater(API_KEY, DOMAIN_NAME, RECORD) - - #get DNS record ip address + # get DNS record ip address previous_ip_address = gandi_updater.get_record_value() logging.debug('DNS record IP address : %s', previous_ip_address) - #get current ip address - current_ip_address = public_ip_retriever.get_public_ip() + # get current ip address + current_ip_address = ipretriever.adapter.get_ip(IP_TRY_COUNT) logging.debug('Current public IP address : %s', current_ip_address) if current_ip_address != previous_ip_address: - #update record value - gandi_updater.update_record_value( current_ip_address, TTL ) + # update record value + gandi_updater.update_record_value(current_ip_address, TTL) logging.info('DNS updated') else: logging.debug('Public IP address unchanged. Nothing to do.') diff --git a/src/ipretriever/adapter.py b/src/ipretriever/adapter.py index befba63..2303da4 100755 --- a/src/ipretriever/adapter.py +++ b/src/ipretriever/adapter.py @@ -4,43 +4,72 @@ import urllib.request import re import ipretriever +import random +import logging -class IfConfig( object ): - def get_public_ip( self ): - """Returns the current public IP address. Raises an exception if an issue occurs.""" - try: - url_page = 'http://ifconfig.me/ip' - public_ip = None +class Generic(object): - f = urllib.request.urlopen(url_page) - data = f.read().decode("utf8") - f.close() - pattern = re.compile('\d+\.\d+\.\d+\.\d+') - result = pattern.search(data, 0) - if result == None: - raise ipretriever.Fault('Service '+url_page+' failed to return the current public IP address') - else: - public_ip = result.group(0) - except urllib.error.URLError as e: - raise ipretriever.Fault(e) - return public_ip - -class IPEcho( object ): - def get_public_ip( self ): + TIMEOUT = 30 + + def __init__(self, url_page): + self.url_page = url_page + + def get_public_ip(self): """Returns the current public IP address. Raises an exception if an issue occurs.""" try: - url_page = 'http://ipecho.net/plain' - public_ip = None - - f = urllib.request.urlopen(url_page) + f = urllib.request.urlopen(self.url_page, timeout=self.TIMEOUT) data = f.read().decode("utf8") f.close() pattern = re.compile('\d+\.\d+\.\d+\.\d+') result = pattern.search(data, 0) - if result == None: - raise ipretriever.Fault('Service '+url_page+' failed to return the current public IP address') + if result is None: + raise ipretriever.Fault('Service ' + self.url_page + ' failed to return the current public IP address') else: - public_ip = result.group(0) + return result.group(0) except urllib.error.URLError as e: raise ipretriever.Fault(e) - return public_ip + +class WhatIsMyIpAddress(Generic): + def __init__(self): + super(WhatIsMyIpAddress, self).__init__('https://ipv4bot.whatismyipaddress.com') + +class WtfIsMyIp(Generic): + def __init__(self): + super(WtfIsMyIp, self).__init__('https://ipv4.wtfismyip.com/text') + +class MyWxternalIp(Generic): + def __init__(self): + super(MyWxternalIp, self).__init__('https://ipv4.myexternalip.com/raw') + +class IpIfy(Generic): + def __init__(self): + super(IpIfy, self).__init__('https://api.ipify.org/?format=raw') + +class IpInfo(Generic): + def __init__(self): + super(IpInfo, self).__init__('https://ipinfo.io/ip') + +ALL = [ + WhatIsMyIpAddress, + WtfIsMyIp, + MyWxternalIp, + IpIfy, + IpInfo, +] + +def get_ip(try_count): + logger = logging.getLogger("get_ip") + errors = [] + for i in range(try_count): + try: + logger.debug("Loop %d/%d", i + 1, try_count) + provider = random.choice(ALL)() + logger.debug("Provider : %s" % provider.url_page) + ip = provider.get_public_ip() + logger.debug("Got ip %s" % provider.url_page) + return ip + except ipretriever.Fault as e: + er = repr(e) + logger.error("Fail to get ip : %s", er) + errors.append(er) + raise ipretriever.Fault("Fail to get ip after %d tries (%s)" % (try_count, ",".join(errors)))