Last active
November 11, 2022 16:25
-
-
Save cvn/7ac82d2ba0f5fa569c4a360387d89438 to your computer and use it in GitHub Desktop.
A script for updating dynamic DNS using Opalstack's API
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.