Skip to content

Instantly share code, notes, and snippets.

@ldotlopez
Last active April 13, 2021 16:27
Show Gist options
  • Save ldotlopez/fbc97b4cc979f18d2a012be42d9bd8ed to your computer and use it in GitHub Desktop.
Save ldotlopez/fbc97b4cc979f18d2a012be42d9bd8ed to your computer and use it in GitHub Desktop.
#!/usr/bin/env python3
import argparse
import logging
import subprocess
from pprint import pprint as pp
import boto3
logging.basicConfig()
logger = logging.getLogger("aws-ddns")
logger.setLevel(logging.DEBUG)
class AWSResponseError(Exception):
def __str__(self):
return (
f"Invalid response from AWS ({self.args[0]['ResponseMetadata']!r})"
)
def get_own_ip():
out = subprocess.check_output(
"dig +short myip.opendns.com @resolver1.opendns.com".split()
)
return out.decode("ascii").strip()
def get_route53_client(aws_access_key_id=None, aws_secret_access_key=None):
if aws_secret_access_key and aws_secret_access_key:
sess = boto3.session.Session(
aws_access_key_id=aws_access_key_id,
aws_secret_access_key=aws_secret_access_key,
)
client = sess.client("route53")
else:
client = boto3.client("route53")
return client
def aws_wrap(fn, *args, **kwargs):
ret = fn(*args, **kwargs)
if ret["ResponseMetadata"]["HTTPStatusCode"] != 200:
raise AWSResponseError(ret)
return ret
def get_aws_hosted_zone_id(client, fqdn):
domain = fqdn.split(".", 1)[1]
resp = aws_wrap(client.list_hosted_zones)
zones_by_name = {x["Name"]: x for x in resp["HostedZones"]}
try:
return zones_by_name[domain]["Id"]
except KeyError as e:
raise KeyError(domain) from e
def get_aws_resource_record(client, zone_id, fqdn):
resp = aws_wrap(client.list_resource_record_sets, HostedZoneId=zone_id)
for record in resp["ResourceRecordSets"]:
if record["Name"] == fqdn:
return record
raise KeyError(fqdn)
def update_aws_dns_record(client, fqdn, record_type="A", record_ttl=300, record_value=None):
if record_value is None:
record_value = get_own_ip()
# Get Zone ID
try:
zone_id = get_aws_hosted_zone_id(client, fqdn)
except AWSResponseError as e:
logger.error(f"Unable to get hosted zones: {e}")
raise
except KeyError as e:
logger.error(f"Domain '{e.args[0]}' not found in hosted zones")
raise
logger.info(f"Found hosted zone '{zone_id}' for '{fqdn}'")
# Get ResourceRecord
try:
fqdn_record = get_aws_resource_record(client, zone_id, fqdn)
except KeyError as e:
fqdn_record = {}
if bool(fqdn_record):
logger.info(f"Domain '{fqdn}' found in zone records.")
record_ips = [x.get("Value") for x in fqdn_record["ResourceRecords"]]
if record_value in record_ips and len(record_ips) == 1:
logger.info(f"Current IP {record_value} is already correct")
return
else:
logger.warning(f"Domain '{fqdn}' not found in zone records, inserting")
update = {
"Comment": "Dynamic DNS",
"Changes": [{"Action": "UPSERT", "ResourceRecordSet": fqdn_record}],
}
update["Changes"][0]["ResourceRecordSet"].update(
{
"Name": fqdn,
"Type": record_type,
"TTL": record_ttl,
"ResourceRecords": [{"Value": record_value}],
}
)
try:
return aws_wrap(
client.change_resource_record_sets,
HostedZoneId=zone_id,
ChangeBatch=update,
)['ChangeInfo']
except AWSResponseError as e:
logger.error(f"Unable to update zone {zone_id} with {update!r}: {e}")
return 0
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("-t", "--type", type=str, default="A")
parser.add_argument("--ttl", type=int, default=300)
parser.add_argument("--ip")
parser.add_argument("--aws-key")
parser.add_argument("--aws-secret")
parser.add_argument("fqdn")
args = parser.parse_args()
if not args.fqdn.endswith("."):
args.fqdn = args.fqdn + '.'
pp(
update_aws_dns_record(
get_route53_client(args.aws_key, args.aws_secret),
args.fqdn,
record_type=args.type,
record_ttl=args.ttl,
record_value=args.ip,
)
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment