Compare commits
9 commits
1f59fcf8db
...
b70621b018
Author | SHA1 | Date | |
---|---|---|---|
b70621b018 | |||
fcb18989b9 | |||
e0b7e598e0 | |||
d504a6a177 | |||
cf926700e4 | |||
0714cbce8a | |||
b55e96d78c | |||
d46419ef3d | |||
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.pyc
|
||||
|
|
17
.project
17
.project
|
@ -1,17 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<projectDescription>
|
||||
<name>gandi_live_dns</name>
|
||||
<comment></comment>
|
||||
<projects>
|
||||
</projects>
|
||||
<buildSpec>
|
||||
<buildCommand>
|
||||
<name>org.python.pydev.PyDevBuilder</name>
|
||||
<arguments>
|
||||
</arguments>
|
||||
</buildCommand>
|
||||
</buildSpec>
|
||||
<natures>
|
||||
<nature>org.python.pydev.pythonNature</nature>
|
||||
</natures>
|
||||
</projectDescription>
|
|
@ -1,8 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<?eclipse-pydev version="1.0"?><pydev_project>
|
||||
<pydev_pathproperty name="org.python.pydev.PROJECT_SOURCE_PATH">
|
||||
<path>/${PROJECT_DIR_NAME}/src</path>
|
||||
</pydev_pathproperty>
|
||||
<pydev_property name="org.python.pydev.PYTHON_PROJECT_VERSION">python 2.7</pydev_property>
|
||||
<pydev_property name="org.python.pydev.PYTHON_PROJECT_INTERPRETER">Default</pydev_property>
|
||||
</pydev_project>
|
13
Dockerfile
Normal file
13
Dockerfile
Normal file
|
@ -0,0 +1,13 @@
|
|||
FROM python:3.7-buster
|
||||
|
||||
LABEL maintainer=david@dme.ninja
|
||||
LABEL version="0.1"
|
||||
|
||||
# Copy scripts and requirements.txt
|
||||
COPY src/ /gandi-live-dns
|
||||
COPY requirements.txt /requirements.txt
|
||||
|
||||
# Install script requirements.txt
|
||||
RUN pip install -r /requirements.txt
|
||||
|
||||
CMD ["python", "/gandi-live-dns/gandi-live-dns.py", "-v", "-r", "3600"]
|
34
README.md
34
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~~ is deprecated since 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:
|
||||
|
@ -88,12 +99,14 @@ Status Code: 201 , DNS Record Created , IP updated for subdomain3
|
|||
|
||||
```
|
||||
root@dyndns:~/gandi-live-dns-master/src# ./gandi-live-dns.py -h
|
||||
usage: gandi-live-dns.py [-h] [-f]
|
||||
usage: gandi-live-dns.py [-h] [-f] [-v] [-r]
|
||||
|
||||
optional arguments:
|
||||
-h, --help show this help message and exit
|
||||
-f, --force force an update/create
|
||||
|
||||
-h, --help show this help message and exit
|
||||
-v, --verbose increase output verbosity
|
||||
-f, --force force an update/create
|
||||
-r REPEAT, --repeat REPEAT
|
||||
keep running and repeat every N seconds
|
||||
```
|
||||
|
||||
The force option runs the script, even when no IP change has been detected.
|
||||
|
@ -124,12 +137,17 @@ Run the script every five minutes.
|
|||
```
|
||||
*/5 * * * * /root/gandi-live-dns-master/src/gandi-live-dns.py >/dev/null 2>&1
|
||||
```
|
||||
|
||||
### Run with Docker
|
||||
|
||||
Use the docker file to build the image. With docker, the script will run every 3600 seconds. (This value can be changed in the Dockerfile.
|
||||
|
||||
### Limitations
|
||||
The XML-RPC API has a limit of 30 requests per 2 seconds, so i guess it's safe to update 25 subdomains at once with the REST API.
|
||||
|
||||
|
||||
### Upcoming Features
|
||||
* command line Argument for verbose mode
|
||||
* ~~command line Argument for verbose mode~~ Aditional verbosity implemented.
|
||||
|
||||
### Inspiration
|
||||
|
||||
|
|
1
requirements.txt
Normal file
1
requirements.txt
Normal file
|
@ -0,0 +1 @@
|
|||
requests
|
|
@ -20,12 +20,22 @@ https://dns.api.gandi.net/api/v5/
|
|||
'''
|
||||
api_endpoint = 'https://dns.api.gandi.net/api/v5'
|
||||
|
||||
#your domain with the subdomains in the zone file/UUID
|
||||
domain = 'mydomain.tld'
|
||||
|
||||
#enter all subdomains to be updated, subdomains must already exist to be updated
|
||||
#your domain and subdomains to be updated, subdomains must already exist to be updated
|
||||
dnsentries = {
|
||||
"mydomain.tld": ["subdomain1", "subdomain2"],
|
||||
"myotherdomain.tld": ["subdomain3"],
|
||||
"mydomain.tld": {
|
||||
"ipv4": ["subdomain1", "subdomain2"],
|
||||
"ipv6": ["subdomain3v6"],
|
||||
},
|
||||
"myotherdomain.tld": {
|
||||
"ipv4": ["subdomain4"],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
#300 seconds = 5 minutes
|
||||
ttl = '300'
|
||||
|
||||
|
@ -36,10 +46,15 @@ run your own external IP provider:
|
|||
+ <?php $ip = $_SERVER['REMOTE_ADDR']; ?>
|
||||
<?php print $ip; ?>
|
||||
e.g.
|
||||
+ https://ifconfig.co
|
||||
+ https://api[4|6].ipify.org
|
||||
+ https://ifconfig.co/ip
|
||||
+ http://ifconfig.me/ip
|
||||
+ http://whatismyip.akamai.com/
|
||||
+ http://ipinfo.io/ip
|
||||
+ many more ...
|
||||
'''
|
||||
ifconfig = 'choose_from_above_or_run_your_own'
|
||||
ifconfig = {
|
||||
"ipv4": 'choose_from_above_or_run_your_own_ipv4_lookup_service',
|
||||
"ipv6": 'choose_from_above_or_run_your_own_ipv6_lookup_service',
|
||||
}
|
||||
|
|
|
@ -1,70 +1,91 @@
|
|||
#!/usr/bin/env python
|
||||
#!/usr/bin/env python3
|
||||
# encoding: utf-8
|
||||
'''
|
||||
Gandi v5 LiveDNS - DynDNS Update via REST API and CURL/requests
|
||||
|
||||
@author: cave
|
||||
@author: dvdme
|
||||
License GPLv3
|
||||
https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
Created on 13 Aug 2017
|
||||
http://doc.livedns.gandi.net/
|
||||
Forked on 08 Dec 2019
|
||||
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
|
||||
import threading as th
|
||||
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.text.strip('\n'))
|
||||
return r.text.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/<DOMAIN>:
|
||||
|
||||
'''
|
||||
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/<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
|
||||
'''
|
||||
if is_ipv6:
|
||||
record_type = '/AAAA'
|
||||
else:
|
||||
record_type = '/A'
|
||||
|
||||
url = config.api_endpoint+ '/zones/' + uuid + '/records/' + config.subdomains[0] + '/A'
|
||||
headers = {"X-Api-Key":config.api_secret}
|
||||
subdomain = config.subdomains[0]
|
||||
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').decode().strip('\n'))
|
||||
return json_object['rrset_values'][0].encode('ascii','ignore').decode().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/<UUID>/records/<NAME>/<TYPE>:
|
||||
curl -X PUT -H "Content-Type: application/json" \
|
||||
|
@ -73,54 +94,83 @@ def update_records(uuid, dynIP, subdomain):
|
|||
"rrset_values": ["<VALUE>"]}' \
|
||||
https://dns.gandi.net/api/v5/zones/<UUID>/records/<NAME>/<TYPE>
|
||||
'''
|
||||
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()
|
||||
|
||||
|
||||
|
||||
def main(force_update, verbosity):
|
||||
def main(force_update, verbosity, repeat):
|
||||
|
||||
if verbosity:
|
||||
print("verbosity turned on - not implemented by now")
|
||||
print('verbosity turned on')
|
||||
verbose = True
|
||||
else:
|
||||
verbose =False
|
||||
|
||||
for key, value in config.dnsentries.items():
|
||||
config.domain = key
|
||||
config.subdomains = value
|
||||
if repeat and verbose:
|
||||
print(f'repeat turned on, will repeat every {repeat} seconds')
|
||||
|
||||
#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)
|
||||
else:
|
||||
if dynIP == dnsIP:
|
||||
print("IP Address Match - no further action")
|
||||
for domain, content in config.dnsentries.items():
|
||||
config.domain = domain
|
||||
for key, value in content.items():
|
||||
afi = key
|
||||
config.subdomains = value
|
||||
|
||||
#get zone ID from Account
|
||||
uuid = get_uuid()
|
||||
|
||||
#compare dynIP and DNS IP
|
||||
dynIP = get_dynip(config.ifconfig[afi], verbose)
|
||||
|
||||
if check_is_ipv6(dynIP, verbose):
|
||||
is_ipv6 = True
|
||||
print('Detected ipv6')
|
||||
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('Detected ipv4')
|
||||
is_ipv6 = False
|
||||
|
||||
if __name__ == "__main__":
|
||||
dnsIP = get_dnsip(uuid, is_ipv6, verbose)
|
||||
|
||||
subdomains = config.subdomains
|
||||
|
||||
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')
|
||||
else:
|
||||
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 repeat:
|
||||
if verbosity:
|
||||
print(f'Repeating in {repeat} seconds')
|
||||
th.Timer(repeat, main, [force_update, verbosity, repeat]).start()
|
||||
|
||||
|
||||
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')
|
||||
parser.add_argument('-r', '--repeat', type=int, help='keep running and repeat every N seconds')
|
||||
args = parser.parse_args()
|
||||
|
||||
main(args.force, args.verbose)
|
||||
main(args.force, args.verbose, args.repeat)
|
||||
|
|
Loading…
Reference in a new issue