IPv6 support and Python 3 conversion
This introduces support to Python 3, since Python 2 will deprecated in January 1st 2020. More info here: https://www.python.org/doc/sunset-python-2/ This Python 3 conversion breaks compatibility with Python 2 but since Python 2 will be deprecated, I see no issue with that. Another update this is introducing is ipv6 support since I started getting my ip in v6 format from the ifconfig providers. There is a new config entry `subdomains6` for configuring the ipv6 domains that will be updated if the returned ip is ipv6. With ipv6, the script will update either ipv6 or ipv4 depending on the value returned by the provider. The usage of the script remains the same. Other changes made: * Use `print()` function instead of the `print` statement * Better readability by using f-string * Use the json object directly from `requests`instead of parsing the content with the `json` module * Add function to check for ipv6 * Add `subdomains6`` to config got ipv6 subdomains * Incresed verbosity option
This commit is contained in:
parent
bcae18b8e8
commit
c1fd36d383
132
.gitignore
vendored
132
.gitignore
vendored
|
@ -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.py
|
||||||
src/config.pyc
|
src/config.pyc
|
||||||
|
|
19
README.md
19
README.md
|
@ -4,7 +4,9 @@ gandi-live-dns
|
||||||
This is a simple dynamic DNS updater for the
|
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.
|
[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.
|
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.
|
key by following their directions.
|
||||||
|
|
||||||
#### A DNS Record
|
#### 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
|
#### 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.
|
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
|
##### subdomains
|
||||||
All subdomains which should be updated. They get created if they do not yet exist.
|
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"]
|
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
|
#### Run the script
|
||||||
And 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
|
### Upcoming Features
|
||||||
* command line Argument for verbose mode
|
* ~~command line Argument for verbose mode~~ Aditional verbosity implemented.
|
||||||
|
|
||||||
### Inspiration
|
### Inspiration
|
||||||
|
|
||||||
|
|
1
requirements.txt
Normal file
1
requirements.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
requests
|
|
@ -1,4 +1,4 @@
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python3
|
||||||
# encoding: utf-8
|
# encoding: utf-8
|
||||||
'''
|
'''
|
||||||
Gandi v5 LiveDNS - DynDNS Update via REST API and CURL/requests
|
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
|
https://www.gnu.org/licenses/gpl-3.0.html
|
||||||
|
|
||||||
Created on 13 Aug 2017
|
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/
|
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 config
|
||||||
import argparse
|
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
|
''' 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
|
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)
|
r = requests.get(ifconfig_provider)
|
||||||
print 'Checking dynamic IP: ' , r._content.strip('\n')
|
print (f'Checking dynamic IP: {r.text.strip()}')
|
||||||
return r.content.strip('\n')
|
return r.text.strip()
|
||||||
|
|
||||||
def get_uuid():
|
def get_uuid(verbose=False):
|
||||||
'''
|
'''
|
||||||
find out ZONE UUID from domain
|
find out ZONE UUID from domain
|
||||||
Info on domain "DOMAIN"
|
Info on domain "DOMAIN"
|
||||||
GET /domains/<DOMAIN>:
|
GET /domains/<DOMAIN>:
|
||||||
|
|
||||||
'''
|
'''
|
||||||
url = config.api_endpoint + '/domains/' + config.domain
|
url = config.api_endpoint + '/domains/' + config.domain
|
||||||
u = requests.get(url, headers={"X-Api-Key":config.api_secret})
|
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:
|
if u.status_code == 200:
|
||||||
return json_object['zone_uuid']
|
return json_object['zone_uuid']
|
||||||
else:
|
else:
|
||||||
print 'Error: HTTP Status Code ', u.status_code, 'when trying to get Zone UUID'
|
print(f'Error: HTTP Status Code {u.status_code} when trying to get Zone UUID')
|
||||||
print json_object['message']
|
pprint(u.json())
|
||||||
exit()
|
exit()
|
||||||
|
|
||||||
def get_dnsip(uuid):
|
def get_dnsip(uuid, is_ipv6=False, verbose=False):
|
||||||
''' find out IP from first Subdomain DNS-Record
|
''' find out IP from first Subdomain DNS-Record
|
||||||
List all records with name "NAME" and type "TYPE" in the zone UUID
|
List all records with name "NAME" and type "TYPE" in the zone UUID
|
||||||
GET /zones/<UUID>/records/<NAME>/<TYPE>:
|
GET /zones/<UUID>/records/<NAME>/<TYPE>:
|
||||||
|
|
||||||
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
|
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'
|
url = config.api_endpoint+ '/zones/' + uuid + '/records/' + subdomain + record_type
|
||||||
headers = {"X-Api-Key":config.api_secret}
|
headers = {'X-Api-Key':config.api_secret}
|
||||||
u = requests.get(url, headers=headers)
|
u = requests.get(url, headers=headers)
|
||||||
if u.status_code == 200:
|
if u.status_code == 200:
|
||||||
json_object = json.loads(u._content)
|
json_object = u.json()
|
||||||
print 'Checking IP from DNS Record' , config.subdomains[0], ':', json_object['rrset_values'][0].encode('ascii','ignore').strip('\n')
|
if verbose:
|
||||||
return json_object['rrset_values'][0].encode('ascii','ignore').strip('\n')
|
pprint(json_object)
|
||||||
|
dnsip = json_object['rrset_values'][0].strip()
|
||||||
|
print (f'Checking IP from DNS Record {subdomain}: {dnsip}')
|
||||||
|
return dnsip
|
||||||
else:
|
else:
|
||||||
print 'Error: HTTP Status Code ', u.status_code, 'when trying to get IP from subdomain', config.subdomains[0]
|
print('Error: HTTP Status Code ', u.status_code, 'when trying to get IP from subdomain', subdomain)
|
||||||
print json_object['message']
|
pprint(u.json())
|
||||||
exit()
|
exit()
|
||||||
|
|
||||||
def update_records(uuid, dynIP, subdomain):
|
def update_records(uuid, dynIP, subdomain, is_ipv6=False, verbose=False):
|
||||||
''' update DNS Records for Subdomains
|
''' update DNS Records for Subdomains
|
||||||
Change the "NAME"/"TYPE" record from the zone UUID
|
Change the "NAME"/"TYPE" record from the zone UUID
|
||||||
PUT /zones/<UUID>/records/<NAME>/<TYPE>:
|
PUT /zones/<UUID>/records/<NAME>/<TYPE>:
|
||||||
curl -X PUT -H "Content-Type: application/json" \
|
curl -X PUT -H "Content-Type: application/json" \
|
||||||
|
@ -73,18 +92,21 @@ def update_records(uuid, dynIP, subdomain):
|
||||||
"rrset_values": ["<VALUE>"]}' \
|
"rrset_values": ["<VALUE>"]}' \
|
||||||
https://dns.gandi.net/api/v5/zones/<UUID>/records/<NAME>/<TYPE>
|
https://dns.gandi.net/api/v5/zones/<UUID>/records/<NAME>/<TYPE>
|
||||||
'''
|
'''
|
||||||
url = config.api_endpoint+ '/zones/' + uuid + '/records/' + subdomain + '/A'
|
if is_ipv6:
|
||||||
payload = {"rrset_ttl": config.ttl, "rrset_values": [dynIP]}
|
record_type = '/AAAA'
|
||||||
headers = {"Content-Type": "application/json", "X-Api-Key":config.api_secret}
|
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)
|
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:
|
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
|
return True
|
||||||
else:
|
else:
|
||||||
print 'Error: HTTP Status Code ', u.status_code, 'when trying to update IP from subdomain', subdomain
|
print (f'Error: HTTP Status Code {u.status_code} when trying to update IP from subdomain {subdomain}')
|
||||||
print json_object['message']
|
print (json_object['message'])
|
||||||
exit()
|
exit()
|
||||||
|
|
||||||
|
|
||||||
|
@ -92,39 +114,46 @@ def update_records(uuid, dynIP, subdomain):
|
||||||
def main(force_update, verbosity):
|
def main(force_update, verbosity):
|
||||||
|
|
||||||
if 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
|
#get zone ID from Account
|
||||||
uuid = get_uuid()
|
uuid = get_uuid()
|
||||||
|
|
||||||
#compare dynIP and DNS IP
|
#compare dynIP and DNS IP
|
||||||
dynIP = get_dynip(config.ifconfig)
|
dynIP = get_dynip(config.ifconfig, verbose)
|
||||||
dnsIP = get_dnsip(uuid)
|
|
||||||
|
if check_is_ipv6(dynIP, verbose):
|
||||||
if force_update:
|
subdomains = config.subdomains6
|
||||||
print "Going to update/create the DNS Records for the subdomains"
|
is_ipv6 = True
|
||||||
for sub in config.subdomains:
|
print('Detected ipv6')
|
||||||
update_records(uuid, dynIP, sub)
|
|
||||||
else:
|
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:
|
if dynIP == dnsIP:
|
||||||
print "IP Address Match - no further action"
|
print ('IP Address Match - no further action')
|
||||||
else:
|
else:
|
||||||
print "IP Address Mismatch - going to update the DNS Records for the subdomains with new IP", dynIP
|
print (f'IP Address Mismatch - going to update the DNS Records for the subdomains with new IP {dynIP}')
|
||||||
for sub in config.subdomains:
|
for sub in subdomains:
|
||||||
update_records(uuid, dynIP, sub)
|
update_records(uuid, dynIP, sub, is_ipv6, verbose)
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == '__main__':
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument('-v', '--verbose', help="increase output verbosity", 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")
|
parser.add_argument('-f', '--force', help='force an update/create', action='store_true')
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
main(args.force, args.verbose)
|
main(args.force, args.verbose)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue