diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..4acd06b
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1 @@
+config.py
diff --git a/.gitignore b/.gitignore
index 6156f13..6732c2d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,174 @@
-src/config.py
-src/config.pyc
+# 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/
+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/
+cover/
+
+# 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
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+# For a library or package, you might want to ignore these files since the code is
+# intended to run in multiple environments; otherwise, check them in:
+# .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
+
+# poetry
+# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
+# This is especially recommended for binary packages to ensure reproducibility, and is more
+# commonly ignored for libraries.
+# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
+#poetry.lock
+
+# pdm
+# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
+#pdm.lock
+# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
+# in version control.
+# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
+.pdm.toml
+.pdm-python
+.pdm-build/
+
+# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
+__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/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
+# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+# and can be added to the global gitignore or merged into this file. For a more nuclear
+# option (not recommended) you can uncomment the following to ignore the entire idea folder.
+#.idea/
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# Custom
+config.py
+config.pyc
diff --git a/.project b/.project
deleted file mode 100644
index 79a2389..0000000
--- a/.project
+++ /dev/null
@@ -1,17 +0,0 @@
-
-
- gandi_live_dns
-
-
-
-
-
- org.python.pydev.PyDevBuilder
-
-
-
-
-
- org.python.pydev.pythonNature
-
-
diff --git a/.pydevproject b/.pydevproject
deleted file mode 100644
index 0ebadbb..0000000
--- a/.pydevproject
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-
-/${PROJECT_DIR_NAME}/src
-
-python 2.7
-Default
-
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..5b727f7
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,14 @@
+FROM python:3.11
+
+LABEL maintainer=https://github.com/dvdme
+LABEL version="1.0.0"
+
+COPY . /usr/src/gandi_live_dns
+WORKDIR /usr/src/gandi_live_dns
+
+# Install script requirements.txt
+RUN pip install -r requirements.txt
+
+ENTRYPOINT ["python3", "gandi_live_dns.py"]
+
+CMD ["python3", "gandi_live_dns.py", "--help"]
diff --git a/README.md b/README.md
index 70fc261..a06d770 100644
--- a/README.md
+++ b/README.md
@@ -1,139 +1,90 @@
-gandi-live-dns
-----
+# 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.
+A dynamic DNS updater for the [Gandi](https://www.gandi.net) registrar. It uses Gandi's [LiveDNS REST API](http://doc.livedns.gandi.net/) to update the zone file for one or more subdomains of a domain to point at the external address 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.
+## Features
-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.
+- Supports multiple subdomains;
+- IPv4 and IPv6 support, depending on the IP lookup service (see table bellow);
+- Creates the subdomain if it not exists;
+- Only tries to update if the IP addresses do not match.
-### Goal
+### IP Lookup Services
-You want your homeserver to be always available at `dynamic_subdomain.mydomain.tld`.
+Different IP lookup service providers are supported. Not all services support both IPv4 and IPv6.
-### Debian Package Requirements
+See table bellow:
-`apt-get update && apt-get upgrade && apt-get install unzip python-requests python-args python-simplejson`
+| Service | IPv4 | IPv6 |
+| ----------------------------- | ------------- | -------------|
+| http://whatismyip.akamai.com | Yes | No |
+| https://ifconfig.co/ip | Yes | Yes |
+| http://ifconfig.me/ip | Yes | Yes |
+| http://ipinfo.io/ip | Yes | No |
-#### API Key
-First, you must apply for an API key with Gandi. Visit
-https://account.gandi.net/en/ and apply for (at least) the production API
-key by following their directions.
+## How to run
-#### A DNS Record
-Create the DNS A Records in the GANDI Webinterface which you want to update if your IP changes.
+A Gandi API Key is needed, see how to get it in [https://api.gandi.net/docs/authentication/](https://api.gandi.net/docs/authentication/).
-#### 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.
+- Clone the repository;
+- Rename `example.config.py` to `config.py`;
+- Set the config with the appropriate values (for reference, see comments in the file);
-or clone from git
+### From git
-`git clone https://github.com/cavebeat/gandi-live-dns.git`
+- Create a virtual environment: `python3 -m venv venv`;
+- Run it: `python3 gandi_live_dns.py`
-#### Script Configuration
-Then you'd need to configure the script in the src directory.
-Copy `example.config.py` to `config.py`, and put it in the same directory as the script.
+### From Docker
-Edit the config file to fit your needs.
+#### Building locally
-##### api_secret
-Start by retrieving your API Key from the "Security" section in new [Gandi Account admin panel](https://account.gandi.net/) to be able to make authenticated requests to the API.
-api_secret = '---my_secret_API_KEY----'
+- `docker build -t gandi-live-dns:local .`
+- `docker run --rm -it -v $(pwd)/config.py:/usr/src/gandi_live_dns/config.py gandi-live-dns:local --force`
-##### api_endpoint
-Gandiv5 LiveDNS API Location
-http://doc.livedns.gandi.net/#api-endpoint
+### Command line options
```
-api_endpoint = 'https://dns.api.gandi.net/api/v5'
+usage: gandi_live_dns.py [-h] [-v] [-f] [-r REPEAT]
+
+options:
+ -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
```
-##### domain
-Your domain for the subdomains to be updated
+choose one as described in the config file.
+### Run continuously
-##### subdomains
-All subdomains which should be updated. They get created if they do not yet exist.
+#### Run with the repeat flag
-```
-subdomains = ["subdomain1", "subdomain2", "subdomain3"]
-```
-The first subdomain is used to find out the actual IP in the Zone Records.
-
-#### Run the script
-And run the script:
-
-```
-root@dyndns:~/gandi-live-dns-master/src# ./gandi-live-dns.py
-Checking dynamic IP: 127.0.0.1
-Checking IP from DNS Record subdomain1: 127.0.0.1
-IP Address Match - no further action
-```
-
-If your IP has changed, it will be detected and the update will be triggered.
-
-
-```
-root@dyndns:~/gandi-live-dns-master/src# ./gandi-live-dns.py
-Checking dynamic IP: 127.0.0.2
-Checking IP from DNS Record subdomain1: 127.0.0.1
-IP Address Mismatch - going to update the DNS Records for the subdomains with new IP 127.0.0.2
-Status Code: 201 , DNS Record Created , IP updated for subdomain1
-Status Code: 201 , DNS Record Created , IP updated for subdomain2
-Status Code: 201 , DNS Record Created , IP updated for subdomain3
-```
+Use `--repeat ` to run continuously.
-#### Command Line Arguments
+Example command with Docker to run continuously in the background:
-```
-root@dyndns:~/gandi-live-dns-master/src# ./gandi-live-dns.py -h
-usage: gandi-live-dns.py [-h] [-f]
-
-optional arguments:
- -h, --help show this help message and exit
- -f, --force force an update/create
-
-```
-
-The force option runs the script, even when no IP change has been detected.
-It will update all subdomains and even create them if they are missing in the
-Zone File/Zone UUID. This can be used if additional/new subdomains get appended to the conig file.
+- `docker run --restart unless-stopped -d -it -v $(pwd)/config.py:/usr/src/gandi_live_dns/config.py gandi-live-dns:local --repeat 1800`
-### IP address lookup service
-There exist several providers for this case, but better is to run your own somewhere.
-
-#### Poor Mans PHP Solution
-On a LAMP Stack, place the file [index.php](https://github.com/cavebeat/gandi-live-dns/blob/master/src/example-index.php) in a directory /ip in your webroot.
+#### Cron the script
+Run the script every five minutes.
```
-root@laptop:~# curl https://blog.cavebeat.org/ip/
-127.0.0.1
+*/5 * * * * python3 gandi-live-dns.py >/dev/null 2>&1
```
-This should fit your personal needs and you still selfhost the whole thing.
-
-#### IP address lookup service https://ifconfig.co
-https://github.com/mpolden/ipd A simple service for looking up your IP address. This is the code that powers [https://ifconfig.co](https://ifconfig.co)
-
-#### use external services
-choose one as described in the config file.
-### Cron the script
+### Issues
-Run the script every five minutes.
-```
-*/5 * * * * /root/gandi-live-dns-master/src/gandi-live-dns.py >/dev/null 2>&1
-```
-### 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.
+Use GitHub issues, avoid sending email's to the mail that is in git history as it is not available anymore.
+### Acknowledgment
-### Upcoming Features
-* command line Argument for verbose mode
+- First ideia: `https://github.com/cavebeat/gandi-live-dns`
-### Inspiration
+#### (Past) Inspiration
-This DynDNS updater is inspired by https://github.com/jasontbradshaw/gandi-dyndns which worked very well
+This DynDNS updater is inspired by https://github.com/jasontbradshaw/gandi-dyndns which worked very well
with the classic DNS from Gandiv4 Website and their XML-RPC API.
-Gandi has created a new API, i accidently switched to the new DNS Record System, so someone had to start a new updater.
+Gandi has created a new API, i accidently switched to the new DNS Record System, so someone had to start a new updater.
diff --git a/example.config.py b/example.config.py
new file mode 100644
index 0000000..8013f59
--- /dev/null
+++ b/example.config.py
@@ -0,0 +1,37 @@
+"""
+Gandi API key
+"""
+api_key = "-- API_KEY --"
+
+"""
+Gandiv5 LiveDNS API Location
+https://api.gandi.net/v5
+"""
+api_endpoint = "https://api.gandi.net/v5"
+
+"""
+Domain to be used
+"""
+domain = "mydomain.tld"
+
+"""
+Subdomains to be updated.
+Subdomains will be created first if not already present
+"""
+subdomains = ["subdomain1", "subdomain2", "subdomain3"]
+
+"""
+DNS record TTL
+300 seconds = 5 minutes
+"""
+ttl = "300"
+
+"""
+IP address lookup service
+"ipclaranet" for "http://ip.clara.net",
+"ipinfoio" for "http://ipinfo.io/ip",
+"ifconfigme" for "http://ifconfig.me/ip",
+"ifconfigco" for "https://ifconfig.co/ip",
+"akamai" for "http://whatismyip.akamai.com/"
+"""
+ifconfig = "choose_from_above_or_run_your_own"
diff --git a/gandi_live_dns.py b/gandi_live_dns.py
new file mode 100644
index 0000000..b13fa53
--- /dev/null
+++ b/gandi_live_dns.py
@@ -0,0 +1,125 @@
+import argparse
+import logging
+import sys
+import time
+
+import requests
+
+import config
+from ip_lookup_services import IPLookupServices
+
+logger = logging.getLogger(__name__)
+LOG_LEVEL = logging.INFO
+logging.basicConfig(
+ stream=sys.stdout,
+ format="%(levelname)s %(asctime)s %(message)s",
+ encoding="utf-8",
+ level=LOG_LEVEL,
+)
+
+
+class GandiDynamicDNS:
+
+ IPV4 = "ipv4"
+ IPV6 = "ipv6"
+
+ def __init__(self, subdomain, domain, force_update=False):
+ self._subdomain = subdomain
+ self._domain = domain
+ self._force_update = force_update
+ self._headers = {
+ "authorization": f"Bearer {config.api_key}",
+ "content-type": "application/json",
+ }
+ self._record_exists = {self.IPV4: False, self.IPV6: False}
+ self._dns_needs_update = {self.IPV4: True, self.IPV6: True}
+ logger.info(f"Subdomain: {self._subdomain}, Domain: {self._domain}")
+ self._update_addresses()
+ self._update_record_exists()
+
+ def _update_addresses(self):
+ self._addresses = IPLookupServices(config.ifconfig).get_result()
+ logger.debug(f"Addresses: {self._addresses}")
+
+ def _update_record_exists(self):
+ for ip_version in [self.IPV4, self.IPV6]:
+ rtype = "AAAA" if ip_version == self.IPV6 else "A"
+ res = requests.get(
+ f"{config.api_endpoint}/livedns/domains/{self._domain}/records/{self._subdomain}/{rtype}",
+ headers=self._headers,
+ timeout=10,
+ )
+ self._record_exists[ip_version] = res.status_code == requests.codes.ok
+ if not self._force_update:
+ dns_value = (
+ res.json()["rrset_values"][0]
+ if self._record_exists[ip_version]
+ and len(res.json()["rrset_values"]) > 0
+ else None
+ )
+ self._dns_needs_update[ip_version] = (
+ not dns_value == self._addresses[ip_version]
+ )
+ logger.debug(f"{ip_version}: {res}")
+
+ def _update_record(self, ip_version, create=False):
+ verb = "POST" if create else "PUT"
+ rtype = "AAAA" if ip_version == self.IPV6 else "A"
+ res = requests.request(
+ verb,
+ f"{config.api_endpoint}/livedns/domains/{self._domain}/records/{self._subdomain}/{rtype}",
+ headers=self._headers,
+ json={
+ "rrset_values": [self._addresses[ip_version]],
+ "rrset_ttl": config.ttl,
+ },
+ timeout=10,
+ )
+ if res.status_code == requests.codes.created:
+ logger.info(
+ f"{'Updated' if verb == 'PUT' else 'Created'} {self._subdomain}.{self._domain} with value {self._addresses[ip_version]}"
+ )
+ else:
+ logger.error(
+ f"Failed to {'update' if verb == 'PUT' else 'Creatcreateed'} {self._subdomain}.{self._domain}"
+ )
+
+ def execute(self):
+ for ip_version in [self.IPV4, self.IPV6]:
+ if self._dns_needs_update[ip_version]:
+ if self._addresses[ip_version] is not None:
+ self._update_record(
+ ip_version, create=not self._record_exists[ip_version]
+ )
+ else:
+ logger.info(
+ f"{self._subdomain}.{self._domain} is correct, skipping update"
+ )
+
+
+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(
+ "-r", "--repeat", type=int, help="keep running and repeat every N seconds"
+ )
+ args = parser.parse_args()
+ if args.verbose:
+ logger.setLevel(logging.DEBUG)
+ try:
+ while True:
+ for item in config.subdomains:
+ gdd = GandiDynamicDNS(item, config.domain, force_update=args.force)
+ gdd.execute()
+ if args.repeat:
+ logger.info(f"Sleeping for {args.repeat} seconds")
+ time.sleep(args.repeat)
+ else:
+ break
+ except KeyboardInterrupt:
+ sys.exit(0)
diff --git a/ip_lookup_services.py b/ip_lookup_services.py
new file mode 100644
index 0000000..ff4c9fe
--- /dev/null
+++ b/ip_lookup_services.py
@@ -0,0 +1,94 @@
+from abc import ABC, abstractmethod
+
+import requests
+
+
+class IPLookupServiceBase(ABC):
+
+ IPV4 = "ipv4"
+ IPV6 = "ipv6"
+ _TIMEOUT = 10
+
+ @property
+ def result(self):
+ _result = {}
+ IPLookupServiceBase.force_ipv4(True)
+ _addr = self.get()
+ _result[self.IPV4] = _addr if self.is_ipv4(_addr) else None
+ IPLookupServiceBase.force_ipv4(False)
+ _addr = self.get()
+ _result[self.IPV6] = _addr if self.is_ipv6(_addr) else None
+ return _result
+
+ def is_ipv4(self, address):
+ return not self.is_ipv6(address)
+
+ def is_ipv6(self, address):
+ return ":" in address
+
+ @abstractmethod
+ def get(self):
+ pass
+
+ @staticmethod
+ def force_ipv4(enable):
+ requests.packages.urllib3.util.connection.HAS_IPV6 = not enable
+
+
+class Akamai(IPLookupServiceBase):
+
+ _SERVICE_ADDRESS = "http://whatismyip.akamai.com/"
+
+ def get(self):
+ return requests.get(self._SERVICE_ADDRESS, timeout=self._TIMEOUT).text.strip()
+
+
+class IfConfigCo(IPLookupServiceBase):
+
+ _SERVICE_ADDRESS = "https://ifconfig.co/ip"
+
+ def get(self):
+ return requests.get(self._SERVICE_ADDRESS, timeout=self._TIMEOUT).text.strip()
+
+
+class IfConfigMe(IPLookupServiceBase):
+
+ _SERVICE_ADDRESS = "http://ifconfig.me/ip"
+
+ def get(self):
+ return requests.get(self._SERVICE_ADDRESS, timeout=self._TIMEOUT).text.strip()
+
+
+class IpInfoIo(IPLookupServiceBase):
+
+ _SERVICE_ADDRESS = "http://ipinfo.io/ip"
+
+ def get(self):
+ return requests.get(self._SERVICE_ADDRESS, timeout=self._TIMEOUT).text.strip()
+
+
+class IpClaraNet(IPLookupServiceBase):
+
+ _SERVICE_ADDRESS = "http://ip.clara.net"
+
+ def get(self):
+ return requests.get(self._SERVICE_ADDRESS, timeout=self._TIMEOUT).text.strip()
+
+
+class IPLookupServices:
+
+ _IP_LOOKUP_SERVICES_MAP = {
+ "ipclaranet": IpClaraNet,
+ "ipinfoio": IpInfoIo,
+ "ifconfigme": IfConfigMe,
+ "ifconfigco": IfConfigCo,
+ "akamai": Akamai,
+ }
+
+ def __init__(self, service_name):
+ self._service_name = service_name.lower()
+ if self._service_name not in self._IP_LOOKUP_SERVICES_MAP.keys():
+ raise ValueError(f"Service {self._service_name} is not known")
+
+ def get_result(self):
+ return self._IP_LOOKUP_SERVICES_MAP[self._service_name]().result
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/.gitignore b/src/.gitignore
deleted file mode 100644
index 3abc68b..0000000
--- a/src/.gitignore
+++ /dev/null
@@ -1,2 +0,0 @@
-/config.py
-/test.py
diff --git a/src/example-index.php b/src/example-index.php
deleted file mode 100644
index 726bc97..0000000
--- a/src/example-index.php
+++ /dev/null
@@ -1,2 +0,0 @@
-
-
diff --git a/src/example.config.py b/src/example.config.py
deleted file mode 100644
index 7ac37fc..0000000
--- a/src/example.config.py
+++ /dev/null
@@ -1,45 +0,0 @@
-'''
-Created on 13 Aug 2017
-@author: cave
-Copy this file to config.py and update the settings
-'''
-#!/usr/bin/env python
-# encoding: utf-8
-
-'''
-Get your API key
-Start by retrieving your API Key from the "Security" section in new Account admin panel to be able to make authenticated requests to the API.
-https://account.gandi.net/
-'''
-api_secret = '---my_secret_API_KEY----'
-
-'''
-Gandiv5 LiveDNS API Location
-http://doc.livedns.gandi.net/#api-endpoint
-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
-subdomains = ["subdomain1", "subdomain2", "subdomain3"]
-
-#300 seconds = 5 minutes
-ttl = '300'
-
-'''
-IP address lookup service
-run your own external IP provider:
-+ https://github.com/mpolden/ipd
-+
-
-e.g.
-+ 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'
diff --git a/src/gandi-live-dns.py b/src/gandi-live-dns.py
deleted file mode 100755
index 55e5757..0000000
--- a/src/gandi-live-dns.py
+++ /dev/null
@@ -1,130 +0,0 @@
-#!/usr/bin/env python
-# encoding: utf-8
-'''
-Gandi v5 LiveDNS - DynDNS Update via REST API and CURL/requests
-
-@author: cave
-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/#api-endpoint -> https://dns.gandi.net/api/v5/
-'''
-
-import requests, json
-import config
-import argparse
-
-
-def get_dynip(ifconfig_provider):
- ''' 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
- '''
- r = requests.get(ifconfig_provider)
- print 'Checking dynamic IP: ' , r._content.strip('\n')
- return r.content.strip('\n')
-
-def get_uuid():
- '''
- 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)
- 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']
- exit()
-
-def get_dnsip(uuid):
- ''' 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 actual DNS Record IP
- '''
-
- url = config.api_endpoint+ '/zones/' + uuid + '/records/' + config.subdomains[0] + '/A'
- 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')
- else:
- print 'Error: HTTP Status Code ', u.status_code, 'when trying to get IP from subdomain', config.subdomains[0]
- print json_object['message']
- exit()
-
-def update_records(uuid, dynIP, subdomain):
- ''' 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" \
- -H 'X-Api-Key: XXX' \
- -d '{"rrset_ttl": 10800,
- "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}
- u = requests.put(url, data=json.dumps(payload), headers=headers)
- json_object = json.loads(u._content)
-
- if u.status_code == 201:
- print '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']
- exit()
-
-
-
-def main(force_update, verbosity):
-
- if verbosity:
- print "verbosity turned on - not implemented by now"
-
-
- #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"
- 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)
-
-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")
- args = parser.parse_args()
-
-
- main(args.force, args.verbose)
-
-
-
-
-
-