Initial version

master
Alinson S. Xavier 5 years ago
commit 2b743de17d

1
.gitignore vendored

@ -0,0 +1 @@
hetzner-ddns.conf

@ -0,0 +1,25 @@
# Hetzner Dynamic DNS Updater
This script finds this machine's hostname, public IPv4 and IPv6 addresses,
then updates the corresponding DNS records on Hetzner.
## Usage
```text
Usage:
hetzner-ddns.py [options]
Options:
-h --help Show this screen
--token=<str> Hetzner API Token
--zone=<str> Name of the DNS zone
--hostname=<std> This machine's hostname
--ttl=<n> Time-to-live in seconds
--v4-api=<url> API that returns your public IPv4 address
--v6-api=<url> API that returns your public IPv6 address
--retry-attempts=<n> Retry N times if connection fails
--retry-delay=<s> Wait S seconds between attempts
--config=<file> Read options from configuration file
--disable-v4 Do not update IPv4 address
--disable-v6 Do not update IPv6 address
```

@ -0,0 +1,3 @@
[example.com]
token=ExAMpLEzLqUTQGBfzMQmxUWXkPL
disable-ipv4=True

@ -0,0 +1,216 @@
#!/usr/bin/env python3
"""Hetzner Dynamic DNS Updater
This script finds this machine's hostname, public IPv4 and IPv6 addresses,
then updates the corresponding DNS records on Hetzner.
Usage:
hetzner-ddns.py [options]
Options:
-h --help Show this screen
--token=<str> Hetzner API Token
--zone=<str> Name of the DNS zone
--hostname=<std> This machine's hostname
--ttl=<n> Time-to-live in seconds
--v4-api=<url> API that returns your public IPv4 address
--v6-api=<url> API that returns your public IPv6 address
--retry-attempts=<n> Retry N times if connection fails
--retry-delay=<s> Wait S seconds between attempts
--config=<file> Read options from configuration file
--disable-v4 Do not update IPv4 address
--disable-v6 Do not update IPv6 address
"""
from docopt import docopt
from time import sleep
import json
import os
import requests
import socket
import sys
import configparser
args = docopt(__doc__)
def merge_config_file():
global args
basepath = os.path.dirname(os.path.abspath(__file__))
candidate_config_files = [
args["--config"],
os.path.join(basepath, "hetzner-ddns.conf"),
"/etc/hetzner-ddns.conf",
]
for filename in candidate_config_files:
if filename is None:
continue
if os.path.isfile(filename):
print("Reading options from %s" % filename)
config = configparser.ConfigParser()
config.read(filename)
section_name = config.sections()[0]
print(" %-20s %s" % ("--zone", section_name))
args["--zone"] = section_name
section = config[section_name]
for key in args.keys():
if key[2:] in section:
value = section[key[2:]]
print(" %-20s %s" % (key, value))
args[key] = value
return args
def merge_defaults():
global args
default_args = {
"--ttl": 300,
"--v4-api": "https://v4.ident.me/",
"--v6-api": "https://v6.ident.me/",
"--retry-attempts": 12,
"--retry-delay": 5,
"--hostname": socket.gethostname(),
}
print("Applying default options:")
for (key, value) in default_args.items():
if args[key] is None:
print(" %-20s %s" % (key, value))
args[key] = value
merge_config_file()
if args["--token"] is None:
print("API token must be provided")
sys.exit(1)
if args["--zone"] is None:
print("DNS zone must be provided")
sys.exit(1)
merge_defaults()
def get_addr(url,
retry=int(args["--retry-attempts"]),
delay=int(args["--retry-delay"])):
exception = None
for i in range(retry):
try:
import urllib
txt = urllib.request.urlopen(url).read()
return txt.decode("utf-8")
except Exception as e:
exception = e
print(" connection failed, retrying in %d seconds..." % delay)
sleep(delay)
raise(exception)
def get_all_records(zone):
response = requests.get(url="https://dns.hetzner.com/api/v1/records",
params={"zone_id": zone["id"]},
headers={"Auth-API-Token": args["--token"]})
return response.json()["records"]
def get_all_zones():
response = requests.get(url="https://dns.hetzner.com/api/v1/zones",
headers={"Auth-API-Token": args["--token"]})
return response.json()["zones"]
def find_record(zone, name, kind="AAAA"):
all_records = get_all_records(zone)
for r in all_records:
if r["type"] == kind and r["name"] == name:
return r
return None
def find_zone(name):
all_zones = get_all_zones()
for z in all_zones:
if z["name"] == name:
return z
raise(Exception("Zone not found: %s" % name))
def update_record(record):
response = requests.put(
url="https://dns.hetzner.com/api/v1/records/%s" % record["id"],
headers={
"Content-Type": "application/json",
"Auth-API-Token": args["--token"],
},
data=json.dumps(record)
)
response.raise_for_status()
def create_record(record):
response = requests.post(
url="https://dns.hetzner.com/api/v1/records",
headers={
"Content-Type": "application/json",
"Auth-API-Token": args["--token"],
},
data=json.dumps(record)
)
response.raise_for_status()
def main():
kinds = []
if not bool(args["--disable-v4"]):
kinds += ["A"]
if not bool(args["--disable-v6"]):
kinds += ["AAAA"]
print("Finding DNS zone...")
zone = find_zone(args["--zone"])
for kind in kinds:
if kind == "A":
print("Finding public IPv4 address...")
addr = get_addr(args["--v4-api"])
print(" %s" % addr)
else:
print("Finding public IPv6 address...")
addr = get_addr(args["--v6-api"])
print(" %s" % addr)
print("Finding existing %s record..." % kind)
rec = find_record(zone=zone,
kind=kind,
name=args["--hostname"])
if rec is None:
print(" not found")
print("Creating new %s record..." % kind)
create_record({
"value": addr,
"type": kind,
"name": args["--hostname"],
"zone_id": args["--zone"],
"ttl": args["--ttl"],
})
print(" done")
else:
print(" found")
print("Updating existing %s record..." % kind)
rec["value"] = addr
rec["ttl"] = args["--ttl"]
update_record(rec)
print(" done")
main()
Loading…
Cancel
Save