Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/.idea/
/src/build/
*.log
*.pyc
118 changes: 62 additions & 56 deletions src/gandyn.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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] <config file>] [-h | --help]')
print(argv[0], ' [[-c | --config] <config file>] [-h | --help]')
print('\t-c --config <config file> : 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,
Expand All @@ -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.')
Expand Down
87 changes: 58 additions & 29 deletions src/ipretriever/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)))