commit
2b743de17d
@ -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…
Reference in new issue