Compare commits
5 Commits
95df5f98cb
...
4bb94bdb61
| Author | SHA1 | Date | |
|---|---|---|---|
| 4bb94bdb61 | |||
| 4751f0e996 | |||
| 2800425740 | |||
| 7a08769359 | |||
| 8ac1ed00b6 |
1
Makefile
1
Makefile
@@ -2,6 +2,7 @@ install:
|
|||||||
cp hetzner-ddns.py /usr/local/bin
|
cp hetzner-ddns.py /usr/local/bin
|
||||||
cp etc/hetzner-ddns.conf.example /etc
|
cp etc/hetzner-ddns.conf.example /etc
|
||||||
cp etc/systemd/hetzner-ddns.service /etc/systemd/system/
|
cp etc/systemd/hetzner-ddns.service /etc/systemd/system/
|
||||||
|
systemctl daemon-reload
|
||||||
|
|
||||||
uninstall:
|
uninstall:
|
||||||
rm /usr/local/bin/hetzner-ddns.py
|
rm /usr/local/bin/hetzner-ddns.py
|
||||||
|
|||||||
29
README.md
29
README.md
@@ -3,6 +3,8 @@
|
|||||||
This script finds this machine's hostname, public IPv4 and IPv6 addresses,
|
This script finds this machine's hostname, public IPv4 and IPv6 addresses,
|
||||||
then updates the corresponding DNS records on Hetzner.
|
then updates the corresponding DNS records on Hetzner.
|
||||||
|
|
||||||
|
**Note:** *This script is not developed, maintained or supported by Hetzner.*
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```text
|
```text
|
||||||
@@ -28,30 +30,39 @@ Options:
|
|||||||
|
|
||||||
The instructions below were tested on Ubuntu Linux 20.04 LTS.
|
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:
|
2. Install package dependencies:
|
||||||
|
|
||||||
pip install docopt
|
```
|
||||||
|
sudo pip3 install docopt
|
||||||
|
```
|
||||||
|
|
||||||
3. Clone the repository somewhere:
|
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:
|
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.
|
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:
|
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
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ After=network-online.target
|
|||||||
Wants=network-online.target
|
Wants=network-online.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=oneshot
|
Type=simple
|
||||||
ExecStart=/usr/local/bin/hetzner-ddns.py
|
ExecStart=python3 -u /usr/local/bin/hetzner-ddns.py
|
||||||
|
|
||||||
[Install]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
|
|||||||
113
hetzner-ddns.py
113
hetzner-ddns.py
@@ -20,6 +20,7 @@ Options:
|
|||||||
--config=<file> Read options from configuration file
|
--config=<file> Read options from configuration file
|
||||||
--disable-v4 Do not update IPv4 address
|
--disable-v4 Do not update IPv4 address
|
||||||
--disable-v6 Do not update IPv6 address
|
--disable-v6 Do not update IPv6 address
|
||||||
|
--repeat=<s> Update DNS again every S seconds
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from docopt import docopt
|
from docopt import docopt
|
||||||
@@ -30,6 +31,7 @@ import requests
|
|||||||
import socket
|
import socket
|
||||||
import sys
|
import sys
|
||||||
import configparser
|
import configparser
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
args = docopt(__doc__)
|
args = docopt(__doc__)
|
||||||
@@ -39,9 +41,9 @@ def merge_config_file():
|
|||||||
global args
|
global args
|
||||||
basepath = os.path.dirname(os.path.abspath(__file__))
|
basepath = os.path.dirname(os.path.abspath(__file__))
|
||||||
candidate_config_files = [
|
candidate_config_files = [
|
||||||
args["--config"],
|
args["--config"],
|
||||||
os.path.join(basepath, "hetzner-ddns.conf"),
|
os.path.join(basepath, "hetzner-ddns.conf"),
|
||||||
"/etc/hetzner-ddns.conf",
|
"/etc/hetzner-ddns.conf",
|
||||||
]
|
]
|
||||||
|
|
||||||
for filename in candidate_config_files:
|
for filename in candidate_config_files:
|
||||||
@@ -65,6 +67,7 @@ def merge_config_file():
|
|||||||
|
|
||||||
return args
|
return args
|
||||||
|
|
||||||
|
|
||||||
def merge_defaults():
|
def merge_defaults():
|
||||||
global args
|
global args
|
||||||
default_args = {
|
default_args = {
|
||||||
@@ -74,6 +77,7 @@ def merge_defaults():
|
|||||||
"--retry-attempts": 12,
|
"--retry-attempts": 12,
|
||||||
"--retry-delay": 5,
|
"--retry-delay": 5,
|
||||||
"--hostname": socket.gethostname(),
|
"--hostname": socket.gethostname(),
|
||||||
|
"--repeat": 3600,
|
||||||
}
|
}
|
||||||
|
|
||||||
print("Applying default options:")
|
print("Applying default options:")
|
||||||
@@ -96,36 +100,40 @@ if args["--zone"] is None:
|
|||||||
merge_defaults()
|
merge_defaults()
|
||||||
|
|
||||||
|
|
||||||
def get_addr(url,
|
def get_addr(
|
||||||
retry=int(args["--retry-attempts"]),
|
url, retry=int(args["--retry-attempts"]), delay=int(args["--retry-delay"])
|
||||||
delay=int(args["--retry-delay"])):
|
):
|
||||||
exception = None
|
exception = None
|
||||||
for i in range(retry):
|
for i in range(retry):
|
||||||
try:
|
try:
|
||||||
import urllib
|
import urllib
|
||||||
|
|
||||||
txt = urllib.request.urlopen(url).read()
|
txt = urllib.request.urlopen(url).read()
|
||||||
return txt.decode("utf-8")
|
return txt.decode("utf-8")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
exception = e
|
exception = e
|
||||||
print(" connection failed, retrying in %d seconds..." % delay)
|
print(" connection failed, retrying in %d seconds..." % delay)
|
||||||
sleep(delay)
|
sleep(delay)
|
||||||
raise(exception)
|
raise (exception)
|
||||||
|
|
||||||
|
|
||||||
def get_all_records(zone):
|
def get_all_records(zone):
|
||||||
response = requests.get(url="https://dns.hetzner.com/api/v1/records",
|
response = requests.get(
|
||||||
params={"zone_id": zone["id"]},
|
url="https://dns.hetzner.com/api/v1/records",
|
||||||
headers={"Auth-API-Token": args["--token"]})
|
params={"zone_id": zone["id"]},
|
||||||
|
headers={"Auth-API-Token": args["--token"]},
|
||||||
|
)
|
||||||
return response.json()["records"]
|
return response.json()["records"]
|
||||||
|
|
||||||
|
|
||||||
def get_all_zones():
|
def get_all_zones():
|
||||||
response = requests.get(url="https://dns.hetzner.com/api/v1/zones",
|
response = requests.get(
|
||||||
headers={"Auth-API-Token": args["--token"]})
|
url="https://dns.hetzner.com/api/v1/zones",
|
||||||
|
headers={"Auth-API-Token": args["--token"]},
|
||||||
|
)
|
||||||
return response.json()["zones"]
|
return response.json()["zones"]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def find_record(zone, name, kind="AAAA"):
|
def find_record(zone, name, kind="AAAA"):
|
||||||
all_records = get_all_records(zone)
|
all_records = get_all_records(zone)
|
||||||
for r in all_records:
|
for r in all_records:
|
||||||
@@ -139,7 +147,7 @@ def find_zone(name):
|
|||||||
for z in all_zones:
|
for z in all_zones:
|
||||||
if z["name"] == name:
|
if z["name"] == name:
|
||||||
return z
|
return z
|
||||||
raise(Exception("Zone not found: %s" % name))
|
raise (Exception("Zone not found: %s" % name))
|
||||||
|
|
||||||
|
|
||||||
def update_record(record):
|
def update_record(record):
|
||||||
@@ -149,7 +157,7 @@ def update_record(record):
|
|||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"Auth-API-Token": args["--token"],
|
"Auth-API-Token": args["--token"],
|
||||||
},
|
},
|
||||||
data=json.dumps(record)
|
data=json.dumps(record),
|
||||||
)
|
)
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
@@ -161,13 +169,25 @@ def create_record(record):
|
|||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
"Auth-API-Token": args["--token"],
|
"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()
|
response.raise_for_status()
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
kinds = []
|
kinds = []
|
||||||
|
delay = int(args["--repeat"])
|
||||||
|
|
||||||
if not bool(args["--disable-v4"]):
|
if not bool(args["--disable-v4"]):
|
||||||
kinds += ["A"]
|
kinds += ["A"]
|
||||||
@@ -178,39 +198,42 @@ def main():
|
|||||||
print("Finding DNS zone...")
|
print("Finding DNS zone...")
|
||||||
zone = find_zone(args["--zone"])
|
zone = find_zone(args["--zone"])
|
||||||
|
|
||||||
for kind in kinds:
|
while True:
|
||||||
if kind == "A":
|
for kind in kinds:
|
||||||
print("Finding public IPv4 address...")
|
if kind == "A":
|
||||||
addr = get_addr(args["--v4-api"])
|
print("Finding public IPv4 address...")
|
||||||
print(" %s" % addr)
|
addr = get_addr(args["--v4-api"])
|
||||||
else:
|
print(" %s" % addr)
|
||||||
print("Finding public IPv6 address...")
|
else:
|
||||||
addr = get_addr(args["--v6-api"])
|
print("Finding public IPv6 address...")
|
||||||
print(" %s" % addr)
|
addr = get_addr(args["--v6-api"])
|
||||||
|
print(" %s" % addr)
|
||||||
|
|
||||||
print("Finding existing %s record..." % kind)
|
print("Finding existing %s record..." % kind)
|
||||||
rec = find_record(zone=zone,
|
rec = find_record(zone=zone, kind=kind, name=args["--hostname"])
|
||||||
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")
|
||||||
|
|
||||||
if rec is None:
|
|
||||||
print(" not found")
|
|
||||||
print("Creating new %s record..." % kind)
|
print("Creating new %s record..." % kind)
|
||||||
create_record({
|
create_record(
|
||||||
"value": addr,
|
{
|
||||||
"type": kind,
|
"value": addr,
|
||||||
"name": args["--hostname"],
|
"type": kind,
|
||||||
"zone_id": args["--zone"],
|
"name": args["--hostname"],
|
||||||
"ttl": args["--ttl"],
|
"zone_id": zone["id"],
|
||||||
})
|
"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")
|
print(" done")
|
||||||
|
|
||||||
|
print(f"Sleeping for {delay} seconds...")
|
||||||
|
time.sleep(delay)
|
||||||
|
|
||||||
main()
|
main()
|
||||||
|
|||||||
Reference in New Issue
Block a user