diff --git a/config-template.txt b/config-template.txt index fb46cffe2d60d55c5538ba8dc58f5e9d1053b241..c0f853c9ee8a6f5fddba72892aad9e32f83376a6 100644 --- a/config-template.txt +++ b/config-template.txt @@ -7,14 +7,15 @@ domain = example.com # A-record name (@, dev, home, etc) that will be updated # example is for raspbian.example.com -a_name = raspbian +a_name = raspbian # TTL (seconds) ttl = 900 # Production API -api = https://dns.api.gandi.net/api/v5/ +gandi_api = https://dns.api.gandi.net/api/v5/ # Number of retries for looking up our own IP address, since # the service seems to be flaky sometimes. retries = 3 +ipify_api = https://api.ipify.org diff --git a/gandi_ddns.py b/gandi_ddns.py index 8b05ebacd5745f2ea432f1b43698a654d0da709b..7653434a973f2a5a6e4596a6882d27b659223c41 100755 --- a/gandi_ddns.py +++ b/gandi_ddns.py @@ -11,114 +11,121 @@ config_file = "config.txt" SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__)) DEFAULT_RETRIES = 3 +DEFAULT_IPIFY_API = "https://api.ipify.org" class GandiDdnsError(Exception): - pass + pass -def get_ip_inner(): - #Get external IP +def get_ip_inner(ipify_api): + # Get external IP + try: + # Could be any service that just gives us a simple raw ASCII IP address (not HTML etc) + r = requests.get(ipify_api, timeout=3) + except requests.exceptions.RequestException: + raise GandiDdnsError('Failed to retrieve external IP.') + if r.status_code != 200: + raise GandiDdnsError( + 'Failed to retrieve external IP.' + ' Server responded with status_code: %d' % r.status_code) + + ip = r.text.rstrip() # strip \n and any trailing whitespace + + if not(ipaddress.IPv4Address(ip)): # check if valid IPv4 address + raise GandiDdnsError('Got invalid IP: ' + ip) + + return ip + + +def get_ip(ipify_api, retries): + # Get external IP with retries + + # Start at 5 seconds, double on every retry. + retry_delay_time = 5 + for attempt in range(retries): try: - # Could be any service that just gives us a simple raw ASCII IP address (not HTML etc) - r = requests.get('https://api.ipify.org', timeout=3) - except requests.exceptions.RequestException: - raise GandiDdnsError('Failed to retrieve external IP.') - if r.status_code != 200: - raise GandiDdnsError( - 'Failed to retrieve external IP.' - ' Server responded with status_code: %d' % r.status_code) - - ip = r.text.rstrip() # strip \n and any trailing whitespace - - if not(ipaddress.IPv4Address(ip)): # check if valid IPv4 address - raise GandiDdnsError('Got invalid IP: ' + ip) - - return ip - - -def get_ip(retries): - #Get external IP with retries - - # Start at 5 seconds, double on every retry. - retry_delay_time = 5 - for attempt in range(retries): - try: - return get_ip_inner() - except GandiDdnsError as e: - print('Getting external IP failed: %s' % e) - print('Waiting for %d seconds before trying again' % retry_delay_time) - time.sleep(retry_delay_time) - # Double retry time, cap at 60s. - retry_delay_time = min(60, 2 * retry_delay_time) + return get_ip_inner(ipify_api) + except GandiDdnsError as e: + print('Getting external IP failed: %s' % e) + print('Waiting for %d seconds before trying again' % retry_delay_time) + time.sleep(retry_delay_time) + # Double retry time, cap at 60s. + retry_delay_time = min(60, 2 * retry_delay_time) print('Exhausted retry attempts') sys.exit(2) + def read_config(config_path): - #Read configuration file - cfg = configparser.ConfigParser() - cfg.read(config_path) + # Read configuration file + cfg = configparser.ConfigParser() + cfg.read(config_path) + + return cfg - return cfg def get_record(url, headers): - #Get existing record - r = requests.get(url, headers=headers) + # Get existing record + r = requests.get(url, headers=headers) + + return r - return r def update_record(url, headers, payload): - #Add record - r = requests.put(url, headers=headers, json=payload) - if r.status_code != 201: - print(('Record update failed with status code: %d' % r.status_code)) - print((r.text)) - sys.exit(2) - print ('Zone record updated.') + # Add record + r = requests.put(url, headers=headers, json=payload) + if r.status_code != 201: + print(('Record update failed with status code: %d' % r.status_code)) + print((r.text)) + sys.exit(2) + print('Zone record updated.') - return r + return r def main(): - path = config_file - if not path.startswith('/'): - path = os.path.join(SCRIPT_DIR, path) - config = read_config(path) - if not config: - sys.exit("Please fill in the 'config.txt' file.") - - for section in config.sections(): - print('%s - section %s' % (str(datetime.now()), section)) - - #Retrieve API key - apikey = config.get(section, 'apikey') - - #Set headers - headers = { 'Content-Type': 'application/json', 'X-Api-Key': '%s' % config.get(section, 'apikey')} - - #Set URL - url = '%sdomains/%s/records/%s/A' % (config.get(section, 'api'), config.get(section, 'domain'), config.get(section, 'a_name')) - print(url) - #Discover External IP - retries = int(config.get(section, 'retries', fallback=DEFAULT_RETRIES)) - external_ip = get_ip(retries) - print(('External IP is: %s' % external_ip)) - - #Prepare record - payload = {'rrset_ttl': config.get(section, 'ttl'), 'rrset_values': [external_ip]} - - #Check current record - record = get_record(url, headers) - - if record.status_code == 200: - print(('Current record value is: %s' % json.loads(record.text)['rrset_values'][0])) - if(json.loads(record.text)['rrset_values'][0] == external_ip): - print('No change in IP address. Goodbye.') - continue - else: - print('No existing record. Adding...') - - update_record(url, headers, payload) + path = config_file + if not path.startswith('/'): + path = os.path.join(SCRIPT_DIR, path) + config = read_config(path) + if not config: + sys.exit("Please fill in the 'config.txt' file.") + + for section in config.sections(): + print('%s - section %s' % (str(datetime.now()), section)) + + # Retrieve API key + apikey = config.get(section, 'apikey') + + # Set headers + headers = {'Content-Type': 'application/json', 'X-Api-Key': '%s' % apikey} + + # Set URL + url = '%sdomains/%s/records/%s/A' % (config.get(section, 'gandi_api'), + config.get(section, 'domain'), config.get(section, 'a_name')) + print(url) + # Discover External IP + ipify_api = config.get(section, 'ipify_api', fallback=DEFAULT_IPIFY_API) + retries = int(config.get(section, 'retries', fallback=DEFAULT_RETRIES)) + external_ip = get_ip(ipify_api, retries) + print(('External IP is: %s' % external_ip)) + + # Prepare record + payload = {'rrset_ttl': config.get(section, 'ttl'), 'rrset_values': [external_ip]} + + # Check current record + record = get_record(url, headers) + + if record.status_code == 200: + print(('Current record value is: %s' % json.loads(record.text)['rrset_values'][0])) + if(json.loads(record.text)['rrset_values'][0] == external_ip): + print('No change in IP address. Goodbye.') + continue + else: + print('No existing record. Adding...') + + update_record(url, headers, payload) + if __name__ == "__main__": - main() + main() diff --git a/test_gandi_ddns.py b/test_gandi_ddns.py index d8235a885ef6916f91cf97807f4338bd39668e9e..e60ce815f04a9d8786d7120d5a51463ac2132153 100644 --- a/test_gandi_ddns.py +++ b/test_gandi_ddns.py @@ -1,5 +1,6 @@ import gandi_ddns as script import requests + def test_get_ip(): - assert script.get_ip() == requests.get("http://ipecho.net/plain?").text \ No newline at end of file + assert script.get_ip("https://api.ipify.org", 3) == requests.get("http://ipecho.net/plain?").text