Compare commits

...

5 Commits

@ -2,6 +2,7 @@ install:
cp hetzner-ddns.py /usr/local/bin
cp etc/hetzner-ddns.conf.example /etc
cp etc/systemd/hetzner-ddns.service /etc/systemd/system/
systemctl daemon-reload
uninstall:
rm /usr/local/bin/hetzner-ddns.py

@ -3,6 +3,8 @@
This script finds this machine's hostname, public IPv4 and IPv6 addresses,
then updates the corresponding DNS records on Hetzner.
**Note:** *This script is not developed, maintained or supported by Hetzner.*
## Usage
```text
@ -28,30 +30,39 @@ Options:
The instructions below were tested on Ubuntu Linux 20.04 LTS.
1. Install `python3` and `pip`:
1. Install Python and pip:
apt install python3
```
sudo apt install python3 python3-pip
```
2. Install package dependencies:
pip install docopt
```
sudo pip3 install docopt
```
3. Clone the repository somewhere:
cd /opt
git clone https://github.com/iSoron/hetzner-ddns.git
```
git clone https://github.com/iSoron/hetzner-ddns.git
```
4. Install the script system-wide:
cd hetzner-ddns
sudo make install
```
cd hetzner-ddns
sudo make install
```
5. Create a configuration file in `/etc/hetzner-ddns.conf` with you API token. See `/etc/hetzner-ddns.conf.example` for an example.
6. Run script and enable it during boot:
sudo systemctl start hetzner-ddns
sudo systemctl enable hetzner-ddns
```
sudo make start
sudo make enable
```
## License

@ -4,8 +4,8 @@ After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/hetzner-ddns.py
Type=simple
ExecStart=python3 -u /usr/local/bin/hetzner-ddns.py
[Install]
WantedBy=multi-user.target

@ -20,6 +20,7 @@ Options:
--config=<file> Read options from configuration file
--disable-v4 Do not update IPv4 address
--disable-v6 Do not update IPv6 address
--repeat=<s> Update DNS again every S seconds
"""
from docopt import docopt
@ -30,6 +31,7 @@ import requests
import socket
import sys
import configparser
import time
args = docopt(__doc__)
@ -39,9 +41,9 @@ 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",
args["--config"],
os.path.join(basepath, "hetzner-ddns.conf"),
"/etc/hetzner-ddns.conf",
]
for filename in candidate_config_files:
@ -51,7 +53,7 @@ def merge_config_file():
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
@ -65,6 +67,7 @@ def merge_config_file():
return args
def merge_defaults():
global args
default_args = {
@ -74,6 +77,7 @@ def merge_defaults():
"--retry-attempts": 12,
"--retry-delay": 5,
"--hostname": socket.gethostname(),
"--repeat": 3600,
}
print("Applying default options:")
@ -96,36 +100,40 @@ if args["--zone"] is None:
merge_defaults()
def get_addr(url,
retry=int(args["--retry-attempts"]),
delay=int(args["--retry-delay"])):
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)
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"]})
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"]})
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:
@ -139,7 +147,7 @@ def find_zone(name):
for z in all_zones:
if z["name"] == name:
return z
raise(Exception("Zone not found: %s" % name))
raise (Exception("Zone not found: %s" % name))
def update_record(record):
@ -149,7 +157,7 @@ def update_record(record):
"Content-Type": "application/json",
"Auth-API-Token": args["--token"],
},
data=json.dumps(record)
data=json.dumps(record),
)
response.raise_for_status()
@ -161,13 +169,25 @@ def create_record(record):
"Content-Type": "application/json",
"Auth-API-Token": args["--token"],
},
data=json.dumps(record)
data=json.dumps(record),
)
response.raise_for_status()
def delete_record(rid):
response = requests.delete(
url=f"https://dns.hetzner.com/api/v1/records/{rid}",
headers={
"Content-Type": "application/json",
"Auth-API-Token": args["--token"],
}
)
response.raise_for_status()
def main():
kinds = []
delay = int(args["--repeat"])
if not bool(args["--disable-v4"]):
kinds += ["A"]
@ -178,39 +198,42 @@ def main():
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")
while True:
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 not None:
if rec["value"] == addr:
print("Existing record is up-to-date")
continue
print("Deleting existing %s record..." % kind)
delete_record(rec["id"])
print(" done")
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)
create_record(
{
"value": addr,
"type": kind,
"name": args["--hostname"],
"zone_id": zone["id"],
"ttl": args["--ttl"],
}
)
print(" done")
print(f"Sleeping for {delay} seconds...")
time.sleep(delay)
main()

Loading…
Cancel
Save