""" export HOSTED_ZONE_ID= export ROUTE53_RECORD=something.mydomain.com """ import logging import os import subprocess import yaml import sys from typing import TYPE_CHECKING, TypedDict import boto3 try: from yaml import CLoader as Loader except ImportError: from yaml import Loader # type: ignore if TYPE_CHECKING: from mypy_boto3_route53 import Route53Client logging.basicConfig( level=logging.INFO, format="%(asctime)s.%(msecs)03d %(levelname)s %(module)s - %(funcName)s: %(message)s", datefmt="%Y-%m-%d %H:%M:%S", ) logger = logging.getLogger(__name__) logger.setLevel(logging.INFO) ROUTE53_RECORDS_FILE = os.getenv("ROUTE53_RECORDS_FILE") GLOBAL_SKIP_IPV4 = os.getenv("GLOBAL_SKIP_IPV4", "false").lower() == "true" GLOBAL_SKIP_IPV6 = os.getenv("GLOBAL_SKIP_IPV6", "false").lower() == "true" class RecordType(TypedDict): record: str hosted_zone_id: str skip_ipv4: bool | None skip_ipv6: bool | None class RecordYamlStruct(TypedDict): records: list[RecordType] def get_ipv4() -> str: result = subprocess.run(["curl", "-4", "ifconfig.me"], capture_output=True) return result.stdout.decode() def get_ipv6() -> str: result = subprocess.run(["curl", "-6", "ifconfig.me"], capture_output=True) return result.stdout.decode() def update_ipv4(hosted_zone_id: str, record: str, public_ipv4: str): client: Route53Client = boto3.client("route53") try: logger.info("Calling upsert for ipv4.") client.change_resource_record_sets( HostedZoneId=hosted_zone_id, ChangeBatch={ "Comment": "Update Public Addresses", "Changes": [ { "Action": "UPSERT", "ResourceRecordSet": { "Name": f"{record}", "Type": "A", "TTL": 300, "ResourceRecords": [{"Value": public_ipv4}], }, } ], }, ) logger.info(f"Successfully updated ipv4 for {record}") except Exception as e: logger.error(f"Error updating ipv4 for {record}.") raise e def update_ipv6(hosted_zone_id: str, record: str, public_ipv6: str): client = boto3.client("route53") try: logger.info("Calling upsert for ipv6.") client.change_resource_record_sets( HostedZoneId=hosted_zone_id, ChangeBatch={ "Comment": "Update Public Addresses", "Changes": [ { "Action": "UPSERT", "ResourceRecordSet": { "Name": f"{record}", "Type": "AAAA", "TTL": 300, "ResourceRecords": [{"Value": public_ipv6}], }, } ], }, ) logger.info(f"Successfully updated ipv6 for {record}") except Exception as e: logger.error(f"Error updating ipv6 for {record}.") raise e def main(): if not ROUTE53_RECORDS_FILE: logger.error("ROUTE53_RECORDS_FILE env var not found!") exit(1) try: with open(ROUTE53_RECORDS_FILE) as f: records_file_contents: RecordYamlStruct = yaml.load(f, Loader) except FileNotFoundError as e: logger.error(e) sys.exit(1) if GLOBAL_SKIP_IPV4: public_ipv4 = None logger.warning("Globally skipping IPv4.") else: logger.info("Getting IPv4 address from ifconfig.me") public_ipv4 = get_ipv4() if not public_ipv4: logger.error("Public IPv4 not found.") exit(1) logger.info(f"Public IPv4 is {public_ipv4}") if GLOBAL_SKIP_IPV6: public_ipv6 = None logger.warning("Globally Skipping IPv6") else: logger.info("Getting IPv6 address from ifconfig.me") public_ipv6 = get_ipv6() if not public_ipv6: logger.error("Public IPv6 not found.") exit(1) logger.info(f"Public IPv6 is {public_ipv6}") for record in records_file_contents["records"]: logger.info(f"Attempting to update {record['record']} from {record['hosted_zone_id']}.") if record.get("skip_ipv4"): logger.info(f"{record['record']} requested to skip IPv4") elif GLOBAL_SKIP_IPV4 or not public_ipv4: logger.info("Globally skipping IPv4") else: update_ipv4( hosted_zone_id=record["hosted_zone_id"], record=record["record"], public_ipv4=public_ipv4, ) if record.get("skip_ipv6"): logger.info(f"{record['record']} requested to skip IPv6") elif GLOBAL_SKIP_IPV6 or not public_ipv6: logger.info("Globally skipping IPv6") else: update_ipv6( hosted_zone_id=record["hosted_zone_id"], record=record["record"], public_ipv6=public_ipv6, ) if __name__ == "__main__": main()