Last active
April 13, 2021 16:27
-
-
Save ldotlopez/fbc97b4cc979f18d2a012be42d9bd8ed to your computer and use it in GitHub Desktop.
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 | |
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