diff --git a/.gitignore b/.gitignore index 6156f13..f85b272 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,134 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# Custom + src/config.py src/config.pyc diff --git a/README.md b/README.md index 70fc261..6c44376 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,9 @@ gandi-live-dns This is a simple dynamic DNS updater for the [Gandi](https://www.gandi.net) registrar. It uses their [LiveDNS REST API](http://doc.livedns.gandi.net/) to update the zone file for a subdomain of a domain to point at the external IPv4 address of the computer it has been run from. -It has been developed on Debian 8 Jessie and tested on Debian 9 Stretch GNU/Linux using Python 2.7. +~~It has been developed on Debian 8 Jessie and tested on Debian 9 Stretch GNU/Linux using Python 2.7.~~ + +This has been update to work with Python 3 (Python 3.6 at Ubuntu 18.04). This will not work with Python 2 since it will be deprecated in 2020-01-01. With the new v5 Website, Gandi has also launched a new REST API which makes it easier to communicate via bash/curl or python/requests. @@ -22,7 +24,10 @@ https://account.gandi.net/en/ and apply for (at least) the production API key by following their directions. #### A DNS Record -Create the DNS A Records in the GANDI Webinterface which you want to update if your IP changes. +Create the DNS A Records in the GANDI Webinterface which you want to update if your IPv4 changes. + +#### AAAA DNS Record (only needed if ipv6 is in use) +Create the DNS AAAA Records for ipv6 in the GANDI Webinterface which you want to update if your IPv6 changes. #### Git Clone or Download the Script Download the Script from here as [zip](https://github.com/cavebeat/gandi-live-dns/archive/master.zip)/[tar.gz](https://github.com/cavebeat/gandi-live-dns/archive/master.tar.gz) and extract it. @@ -56,10 +61,16 @@ Your domain for the subdomains to be updated ##### subdomains All subdomains which should be updated. They get created if they do not yet exist. +* `subdomains` for ipv4 +* `subdomains6` for ipv6 + ``` subdomains = ["subdomain1", "subdomain2", "subdomain3"] +subdomains6 = ["subdomain1v6", "subdomain2v6", "subdomain3v6"] ``` -The first subdomain is used to find out the actual IP in the Zone Records. + +The first subdomain is used to find out the actual IP in the Zone Records. +If the returnded ip from is ipv6, it will use the first from subdomains6. #### Run the script And run the script: @@ -129,7 +140,7 @@ The XML-RPC API has a limit of 30 requests per 2 seconds, so i guess it's safe t ### Upcoming Features -* command line Argument for verbose mode +* ~~command line Argument for verbose mode~~ Aditional verbosity implemented. ### Inspiration diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..f229360 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +requests diff --git a/src/gandi-live-dns.py b/src/gandi-live-dns.py index 55e5757..f17de86 100755 --- a/src/gandi-live-dns.py +++ b/src/gandi-live-dns.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # encoding: utf-8 ''' Gandi v5 LiveDNS - DynDNS Update via REST API and CURL/requests @@ -8,63 +8,82 @@ License GPLv3 https://www.gnu.org/licenses/gpl-3.0.html Created on 13 Aug 2017 -http://doc.livedns.gandi.net/ +http://doc.livedns.gandi.net/ http://doc.livedns.gandi.net/#api-endpoint -> https://dns.gandi.net/api/v5/ ''' -import requests, json +import json +import requests +import ipaddress import config import argparse +from pprint import pprint -def get_dynip(ifconfig_provider): +def check_is_ipv6(ip_address, verbose=False): + return ipaddress.ip_address(ip_address).version == 6 + + +def get_dynip(ifconfig_provider, verbose=False): ''' find out own IPv4 at home <-- this is the dynamic IP which changes more or less frequently similar to curl ifconfig.me/ip, see example.config.py for details to ifconfig providers - ''' + ''' + if verbose: + print(f'Using {ifconfig_provider}') r = requests.get(ifconfig_provider) - print 'Checking dynamic IP: ' , r._content.strip('\n') - return r.content.strip('\n') + print (f'Checking dynamic IP: {r.text.strip()}') + return r.text.strip() -def get_uuid(): - ''' +def get_uuid(verbose=False): + ''' find out ZONE UUID from domain Info on domain "DOMAIN" GET /domains/: - ''' url = config.api_endpoint + '/domains/' + config.domain u = requests.get(url, headers={"X-Api-Key":config.api_secret}) - json_object = json.loads(u._content) + json_object = u.json() + if verbose: + pprint(json_object) if u.status_code == 200: return json_object['zone_uuid'] else: - print 'Error: HTTP Status Code ', u.status_code, 'when trying to get Zone UUID' - print json_object['message'] + print(f'Error: HTTP Status Code {u.status_code} when trying to get Zone UUID') + pprint(u.json()) exit() -def get_dnsip(uuid): +def get_dnsip(uuid, is_ipv6=False, verbose=False): ''' find out IP from first Subdomain DNS-Record List all records with name "NAME" and type "TYPE" in the zone UUID GET /zones//records//: - - The first subdomain from config.subdomain will be used to get + + The first subdomain from config.subdomain will be used to get the actual DNS Record IP ''' + if is_ipv6: + record_type = '/AAAA' + subdomain = config.subdomains6[0] + else: + record_type = '/A' + subdomain = config.subdomains[0] - url = config.api_endpoint+ '/zones/' + uuid + '/records/' + config.subdomains[0] + '/A' - headers = {"X-Api-Key":config.api_secret} + url = config.api_endpoint+ '/zones/' + uuid + '/records/' + subdomain + record_type + headers = {'X-Api-Key':config.api_secret} u = requests.get(url, headers=headers) if u.status_code == 200: - json_object = json.loads(u._content) - print 'Checking IP from DNS Record' , config.subdomains[0], ':', json_object['rrset_values'][0].encode('ascii','ignore').strip('\n') - return json_object['rrset_values'][0].encode('ascii','ignore').strip('\n') + json_object = u.json() + if verbose: + pprint(json_object) + dnsip = json_object['rrset_values'][0].strip() + print (f'Checking IP from DNS Record {subdomain}: {dnsip}') + return dnsip else: - print 'Error: HTTP Status Code ', u.status_code, 'when trying to get IP from subdomain', config.subdomains[0] - print json_object['message'] + print('Error: HTTP Status Code ', u.status_code, 'when trying to get IP from subdomain', subdomain) + pprint(u.json()) exit() -def update_records(uuid, dynIP, subdomain): - ''' update DNS Records for Subdomains +def update_records(uuid, dynIP, subdomain, is_ipv6=False, verbose=False): + ''' update DNS Records for Subdomains Change the "NAME"/"TYPE" record from the zone UUID PUT /zones//records//: curl -X PUT -H "Content-Type: application/json" \ @@ -73,18 +92,21 @@ def update_records(uuid, dynIP, subdomain): "rrset_values": [""]}' \ https://dns.gandi.net/api/v5/zones//records// ''' - url = config.api_endpoint+ '/zones/' + uuid + '/records/' + subdomain + '/A' - payload = {"rrset_ttl": config.ttl, "rrset_values": [dynIP]} - headers = {"Content-Type": "application/json", "X-Api-Key":config.api_secret} + if is_ipv6: + record_type = '/AAAA' + else: + record_type = '/A' + url = config.api_endpoint+ '/zones/' + uuid + '/records/' + subdomain + record_type + payload = {'rrset_ttl': config.ttl, "rrset_values": [dynIP]} + headers = {'Content-Type': 'application/json', 'X-Api-Key':config.api_secret} u = requests.put(url, data=json.dumps(payload), headers=headers) - json_object = json.loads(u._content) - + json_object = u.json() if u.status_code == 201: - print 'Status Code:', u.status_code, ',', json_object['message'], ', IP updated for', subdomain + print (f'Status Code: {u.status_code}, {json_object["message"]}, IP updated for {subdomain}') return True else: - print 'Error: HTTP Status Code ', u.status_code, 'when trying to update IP from subdomain', subdomain - print json_object['message'] + print (f'Error: HTTP Status Code {u.status_code} when trying to update IP from subdomain {subdomain}') + print (json_object['message']) exit() @@ -92,39 +114,46 @@ def update_records(uuid, dynIP, subdomain): def main(force_update, verbosity): if verbosity: - print "verbosity turned on - not implemented by now" + print('verbosity turned on') + verbose = True + else: + verbose =False - #get zone ID from Account uuid = get_uuid() - - #compare dynIP and DNS IP - dynIP = get_dynip(config.ifconfig) - dnsIP = get_dnsip(uuid) - - if force_update: - print "Going to update/create the DNS Records for the subdomains" - for sub in config.subdomains: - update_records(uuid, dynIP, sub) + + #compare dynIP and DNS IP + dynIP = get_dynip(config.ifconfig, verbose) + + if check_is_ipv6(dynIP, verbose): + subdomains = config.subdomains6 + is_ipv6 = True + print('Detected ipv6') else: + print('Detected ipv4') + is_ipv6 = False + subdomains = config.subdomais + + dnsIP = get_dnsip(uuid, is_ipv6, verbose) + + if force_update: + print ('Going to update/create the DNS Records for the subdomains') + for sub in subdomains: + update_records(uuid, dynIP, sub, is_ipv6, verbose) + else: + if verbose: + print(f'dynIP: {dynIP}') + print(f'dnsIP: {dnsIP}') if dynIP == dnsIP: - print "IP Address Match - no further action" + print ('IP Address Match - no further action') else: - print "IP Address Mismatch - going to update the DNS Records for the subdomains with new IP", dynIP - for sub in config.subdomains: - update_records(uuid, dynIP, sub) + print (f'IP Address Mismatch - going to update the DNS Records for the subdomains with new IP {dynIP}') + for sub in subdomains: + update_records(uuid, dynIP, sub, is_ipv6, verbose) -if __name__ == "__main__": +if __name__ == '__main__': parser = argparse.ArgumentParser() - parser.add_argument('-v', '--verbose', help="increase output verbosity", action="store_true") - parser.add_argument('-f', '--force', help="force an update/create", action="store_true") + parser.add_argument('-v', '--verbose', help='increase output verbosity', action='store_true') + parser.add_argument('-f', '--force', help='force an update/create', action='store_true') args = parser.parse_args() - - main(args.force, args.verbose) - - - - - -