-
-
Save cvn/7ac82d2ba0f5fa569c4a360387d89438 to your computer and use it in GitHub Desktop.
#!/usr/bin/env python3 | |
# update_ip.py - A script for updating dynamic DNS using Opalstack's API | |
# by Chad von Nau | |
# | |
# * Python 2 and 3 compatible. | |
# * Supports IPv4 and IPv6 addresses. | |
# * Records the last IP in a JSON file and only calls the API if the IP has changed. | |
# | |
# Instructions: | |
# | |
# 1. Create a new DNS Record on Opalstack, with the domain you want to use. | |
# 2. Create an Opalstack API token, if you don't already have one. | |
# 3. Create a PHP app on Opalstack, with this as the index.php: | |
# <?php echo $_SERVER['REMOTE_ADDR']; ?> | |
# 4. Set the variables below. | |
# 5. Run this script, pass the domain as an argument, ex: | |
# python update_ip.py my-domain.com | |
# 6. Optionally, automate this to run periodically. I run it every hour using cron. | |
# To minimize logging, I use `sleep` to wait for WiFi after machine wake, and I redirect stdout to /dev/null. | |
# sleep 20 && /usr/local/bin/python3 /Users/example/update_ip.py example.com > /dev/null | |
# Variables | |
token = 'YOUR_OPALSTACK_API_TOKEN' | |
ip_service = 'http://YOUR_PHP_APP_URL' # expect service to return IP address as string | |
# Imports | |
import os | |
import sys | |
import json | |
import argparse | |
try: | |
# python 3 | |
from urllib.request import Request, urlopen | |
from urllib.error import URLError | |
except ImportError: | |
# python 2 | |
from urllib2 import Request, urlopen, URLError | |
# Command line arguments | |
parser = argparse.ArgumentParser(description="Update dynamic DNS using Opalstack's API.") | |
parser.add_argument('domain', help="domain name") | |
parser.add_argument('-f', '--force', action='store_true', help="force update") | |
args = parser.parse_args() | |
domain = args.domain | |
force = args.force | |
# Get IP address from remote service | |
try: | |
current_ip = urlopen(ip_service).read().decode('utf-8') | |
except URLError: | |
try: | |
urlopen('https://www.google.com') | |
sys.exit('Error: Could not connect to IP service "{}". Not updating.'.format(ip_service)) | |
except URLError: | |
# Print to stdout instead of stderr because, for logging purposes, we don't want to treat it as an error. | |
print('No internet. Not updating.') | |
sys.exit(1) | |
if not current_ip: | |
sys.exit('Error: No IP returned by IP service "{}". Not updating.'.format(ip_service)) | |
# Get file path of info file | |
filename = 'info-{}.json'.format(domain) | |
path = os.path.dirname(os.path.realpath(__file__)) | |
# strip out any nasty characters | |
# http://stackoverflow.com/questions/7406102/create-sane-safe-filename-from-any-unsafe-string | |
keep_characters = ('-','.','_') | |
filename = ''.join(c for c in filename if c.isalnum() or c in keep_characters).rstrip() | |
info_file = '{}/{}'.format(path, filename) | |
# Load data from info file | |
if os.path.isfile(info_file): | |
with open(info_file, 'r') as f: | |
info = json.load(f) | |
else: | |
info = {} | |
# Define util function | |
def api_request(url, data=None): | |
headers = { | |
'Authorization': "Token {}".format(token), | |
'Content-Type': 'application/json', | |
} | |
data = json.dumps(data).encode() if data else None | |
req = Request(url, data, headers=headers) | |
resp = urlopen(req).read() | |
return json.loads(resp) | |
# Update record if necessary | |
if force or info.get('content') != current_ip: | |
dns_uuid = info.get('id') | |
domain_uuid = info.get('domain') | |
# Get UUIDs if necessary | |
if not (dns_uuid and domain_uuid): | |
dns_list = api_request('https://my.opalstack.com/api/v1/dnsrecord/list/?embed=domain') | |
item = next((item for item in dns_list if item.get('domain').get('name') == domain), None) | |
if not item: | |
sys.exit('Error: No DNS record found for "{}"'.format(domain)) | |
dns_uuid = item.get('id') | |
domain_uuid = item.get('domain').get('id') | |
# Set the IP address on Opalstack. | |
update_data = { | |
"id" : dns_uuid, | |
"domain" : domain_uuid, | |
"type" : "AAAA" if ':' in current_ip else "A", | |
"content" : current_ip, | |
} | |
dns_update = api_request('https://my.opalstack.com/api/v1/dnsrecord/update/', [update_data]) | |
# Write response to file | |
with open(info_file, 'w') as f: | |
json.dump(dns_update[0], f, indent=2) | |
print('IP updated to %s' % current_ip) | |
else: | |
print('IP has not changed (%s). Not updating.' % current_ip) |
I updated the script to use the v1 API. Thanks for the heads up, @mightyohm.
The new script appears to be working. Thanks for updating it!
You should consider lowering the TTL value to the lowest possible Opalstack allows, especially if you are using this to self-host services in your home.
The lower the TTL, the faster the change will propagate across the internet.
That’s not a bad idea, @elromanos. The current value fits my use case, and it’s easy enough to change, so I’m going to leave the script as-is for now.
Another interesting alternative is to get the TTL value from the existing DNS record, instead of setting it explicitly in the script. This would allow you to have different TTL values for each record, and manage them all in the Opalstack UI.
I've updated the script with better logging. Info messages will be output to stdout, and errors to stderr. So, you can control what you log more easily. In my case, I redirect stdout to /dev/null when I use it with cron, so I only log when something is wrong. See the instructions for an example.
@elromanos the script no longer sets the TTL, and so it will preserve the existing TTL on a record.
Looks like this script is broken as of this week (the v0 API is now returning 404).