From 0b245aea4c1e32c4f39ac554880d6ed8c21be8d1 Mon Sep 17 00:00:00 2001 From: "Gangwoo \"Peter\" Cho" Date: Fri, 4 Feb 2022 18:22:54 -0500 Subject: [PATCH] added Name field support - version 0.0.7 --- code/ddns-lambda.py | 1187 +++++++++++++------------------------------ 1 file changed, 352 insertions(+), 835 deletions(-) diff --git a/code/ddns-lambda.py b/code/ddns-lambda.py index 1ecccb1..9e8bd49 100755 --- a/code/ddns-lambda.py +++ b/code/ddns-lambda.py @@ -1,26 +1,48 @@ -# To do -# 1. read custom host name field to use that as DNS over IP address - """ -DDNS Lambda Python3 Script - -This script will perform the following functions. - -if no CNAME or ZONE tags is set on the ec2 instance, and not using a custom dhcp option set: -1. Script will do nothing - -if no CNAME or ZONE tags are set, but are using a custom dhcp option set with -a hosted zone created, which matches the domain name. -1. An 'A' record is created to the IP -2. A 'PTR" record is create to the DNS name - -if a CNAME tag is set. -1. Creates a CNAME to the DNS name -2. Creates a PTR record to the CNAME - -if a ZONE tag is set. -1. Creates an 'A' record to the IP -2. Creates a 'PTR" record to the DNS name +DDNS Lambda Python3 Script (revised) + +The script will retrieve the information from the EC2 instance or from the DynamoDB Table entry. +IF the instance is running, it will create the DynamoDB Item with the instance data. +* PrivateIpAddress +* PrivateDnsName +* SubnetId +* VpcId + +Withe the information will check several pre-reqs: +1. DHCP configuration exists on the VPC that has the domain name (Route 53 PHZ) that is associated with the VPC. +2. DNS hostname enabled on the VPC +3. DNS support enabled on the VPC +4. Reverse Lookup zone (Route 53 PHZ) for the subnet is associated with the VPC. + +It will then itereate through the Tags and based upon the Tag combination, +construct the A/PTR record to be created (running) or deleted (shutting down) + +The following tag combination will result in the A/PTR record. +The order matters since first match will skip the rest of the condition. +1. If Custom hostname Tag AND Custom Zone Tags exist and valid +- hostname: Custom Hostname +- zonename: Custom Zonename +2. If Custom hostname Tag is valid AND there's NO Custom ZOone Tag AND VPC Domain name is Valid +- hostname: Custom Hostname +- zonename: VPC Domain name given via DHCP option +3. If Name Tag is valid AND VPC Domain name is Valid +- hostname: Name (minus the zone name) +- zonename: VPC Domain name given via DHCP option +4. Custom Zone Tags is valid AND Custom Hostname is NOT Valid (or empty) +- hostname: ip-1-2-3-4 format +- zonename: Custom Zonename +5. No Custom Tags +- hostname: ip-1-2-3-4 format +- zonename: VPC Domain name given via DHCP option +6. If no match above then exit out script (no A/PTR record) + +Once the assignment is made above, the script will either create the A/PTR record (if running) +or delete the A/PTR record (if shutting down) + +If the CNAME tag is present and valid, then the script will create/delete the CNAME in the proper +zone with the value that points to the hostname+zonename created above. + +Finally, it will clean up the DynamoDB entry if the instance is shutting down. """ import json import sys @@ -33,17 +55,17 @@ import inspect import boto3 import os +import ipaddress from botocore.exceptions import ClientError # Setting Global Variables LOGGER = logging.getLogger() ACCOUNT = None REGION = None -VERSION = '0.0.6' +VERSION = '0.0.7' # Adjust the logging level [logging.INFO, logging.DEBUG, logging.WARNING, etc] LOGGER.setLevel(logging.DEBUG) -# SNS_CLIENT = None # Read Env variables SLEEPTIME = int(os.environ.get('SleepTime', '60')) @@ -65,17 +87,6 @@ def lineno(): # pragma: no cover """ return str(' - line number: ' + str(inspect.currentframe().f_back.f_lineno)) -# def get_sns_client(): -# """ -# Get sns client -# :return: -# """ -# try: -# return boto3.client('sns') -# except ClientError as err: -# print("Unexpected error: %s" % err) - - def get_route53_client(): """ Get route53 client @@ -131,7 +142,6 @@ def lambda_handler( """ LOGGER.info("event: %s", str(event) + lineno()) LOGGER.info("context: %s", str(context) + lineno()) - # SNS_CLIENT = sns_client caller_response = [] # Checking to make sure there is a dynamodb table named in the Env Variable @@ -143,8 +153,6 @@ def lambda_handler( else: LOGGER.info('DynamoDB table does not exist, exiting function: %s', DDBNAME) return None - # commented out by awspeter - # create_table(dynamodb_client, DDBNAME) # Set variables # Get the state from the Event stream @@ -154,9 +162,7 @@ def lambda_handler( # Get the instance id, region, and tag collection instance_id = event['detail']['instance-id'] LOGGER.debug("instance id: %s", str(instance_id) + lineno()) - #ACCOUNT = event['account'] region = event['region'] - #REGION = region LOGGER.debug("region: %s", str(region) + lineno()) # Only doing something if the state is running @@ -201,19 +207,6 @@ def lambda_handler( LOGGER.debug("tags are: %s", str(tags) + lineno()) - # tag_type = determine_tag_type(tags) - # changed to return a list, so read the 1st value - tag_type = determine_tag_type(tags)[0] - has_custom_hostname = determine_tag_type(tags)[1] # if hostname is found in the - - LOGGER.debug("tag type: %s", str(tag_type) + lineno()) - LOGGER.debug("has custom hostname tag: %s", str(has_custom_hostname) + lineno()) - - if tag_type == 'invalid': - LOGGER.info( - "Must have either CNAME or ZONE in tags, can not have both tags" + lineno()) - exit(-1) - LOGGER.debug("Get instance attributes %s", lineno()) LOGGER.debug("instance: %s", str(instance) + lineno()) LOGGER.debug("type: %s", str(type(instance)) + lineno()) @@ -233,32 +226,6 @@ def lambda_handler( LOGGER.debug("private_dns_name: %s", str(private_dns_name) + lineno()) LOGGER.debug("private_host_name: %s", str(private_host_name) + lineno()) - # awspeter - commneted out public dns - # public_ip = None - # public_dns_name = None - - # awspeter - commneted out public dns - # if 'PublicIpAddress' in instance['Reservations'][0]['Instances'][0]: - # LOGGER.debug('instance has public ip address key') - # try: - # LOGGER.debug("instance: %s", str(instance) + lineno()) - # if 'Reservations' in instance: - # LOGGER.debug("reservations: %s", str(instance['Reservations'][0])) - # if 'Instances' in instance['Reservations'][0]: - # LOGGER.debug("instances: %s", str(instance['Reservations'][0]['Instances'][0])) - # public_ip = instance['Reservations'][0]['Instances'][0]['PublicIpAddress'] - # LOGGER.debug("public_ip: %s", str(public_ip) + lineno()) - # if public_ip and 'PublicDnsName' not in instance['Reservations'][0]['Instances'][0]: - # LOGGER.info("Could not find PublicDnsName for public instance, check that vpc has dns hostnames enabled:" + lineno()) - # exit() - # else: - # public_dns_name = instance['Reservations'][0]['Instances'][0]['PublicDnsName'] - # LOGGER.debug("public_dns_name: %s", str(public_dns_name) + lineno()) - # public_host_name = public_dns_name.split('.')[0] - # LOGGER.debug("public_host_name: %s", str(public_host_name)) - # except BaseException as err: - # LOGGER.info("Unexpected error: %s", str(err)) - # Get the subnet mask of the instance subnet_id = instance['Reservations'][0]['Instances'][0]['SubnetId'] LOGGER.debug("subnet_id: %s", str(subnet_id) + lineno()) @@ -293,7 +260,6 @@ def lambda_handler( vpc_id), lineno()) exit() - # Create the public and private hosted zone collections. # These are collections of zones in Route 53. hosted_zones = list_hosted_zones(route53) LOGGER.debug("hosted_zones: %s", str(hosted_zones) + lineno()) @@ -304,19 +270,9 @@ def lambda_handler( LOGGER.debug("private_hosted_zone_collection: %s", str(list(private_hosted_zone_collection)) + lineno()) - # awspeter - commneted out public dns - # public_hosted_zones = get_public_hosted_zones(hosted_zones) - # LOGGER.debug("public_hosted_zones: %s", str(list(public_hosted_zones)) + lineno()) - # public_hosted_zones_collection = get_public_hosted_zone_collection(public_hosted_zones) - # LOGGER.debug("public_hosted_zones_collection:" - # " %s", str(list(public_hosted_zones_collection)) + lineno()) - # Check to see whether a reverse lookup zone for the instance # already exists. If it does, check to see whether - # the reverse lookup zone is associated with the instance's - # VPC. If it isn't create the association. You don't - # need to do this when you create the reverse lookup - # zone because the association is done automatically. + # the reverse lookup zone is associated with the instance's VPC. LOGGER.info("reversed_lookup_zone: %s", str(reversed_lookup_zone) + lineno()) reverse_zone = None for record in hosted_zones['HostedZones']: @@ -345,241 +301,104 @@ def lambda_handler( reverse_lookup_zone_id, vpc_id, lineno()) reverse_zone_associated = False - # awspeter - commmeted out vpc association - # LOGGER.info("Associating zone %s with VPC %s", reverse_lookup_zone_id, vpc_id) - # try: - # associate_zone(route53, reverse_lookup_zone_id, region, vpc_id) - # except BaseException as err: - # LOGGER.debug("%s", str(err)+lineno()) else: LOGGER.info( "No matching reverse lookup zone, PTR record will not be created %s", lineno()) - # LOGGER.info("No matching reverse lookup zone, so we will create one %s", lineno()) - # # create private hosted zone for reverse lookups - # if state == 'running': - # create_reverse_lookup_zone(route53, instance, reversed_domain_prefix, region) - # reverse_lookup_zone_id = get_zone_id(route53, reversed_lookup_zone) # Wait a random amount of time. This is a poor-mans back-off # if a lot of instances are launched all at once. time.sleep(random.random()) - if tag_type == 'cname_selected': - # We must have a cname because we want reverse dns to point to the A record - cname = get_cname_from_tags(tags) - cname_prefix = cname.split('.')[0] - # if not cname: - # publish_to_sns( - # SNS_CLIENT, - # ACCOUNT, REGION, - # "Must have a CNAME tag for lambda to work. " - # "Please add CNAME to instance tags" + lineno() - # ) + # Is there a DHCP option set? + # Get DHCP option set configuration + LOGGER.debug("\n#############\nIterate over DHCP option sets %s\n", lineno()) + + try: + LOGGER.debug("trying to get dhcp option set id %s", lineno()) + dhcp_options_id = get_dhcp_option_set_id_for_vpc(compute, vpc_id) + LOGGER.debug("dhcp_options_id: %s", str(dhcp_options_id) + lineno()) + dhcp_configurations = get_dhcp_configurations(compute, dhcp_options_id) + LOGGER.debug("dhcp_configurations: %s", str(get_dhcp_configurations) + lineno()) + + except BaseException as err: + LOGGER.info("No DHCP option set assigned to this VPC %s\n", str(err) + lineno()) + exit() + + # Look to see whether there's a DHCP option set assigned to + # the VPC. Obtain the zone name, and check if there is a Private Hosted Zone + # associated with the VPC. If so, it will set the zone name to be used later. + has_dhcp_dns_zone_associated_vpc = False + + for configuration in dhcp_configurations: + LOGGER.debug("configuration: %s", str(configuration) + lineno()) + LOGGER.debug("private hosted zones: %s", str( + private_hosted_zone_collection) + lineno()) + + if configuration in private_hosted_zone_collection: + private_hosted_zone_name = configuration + LOGGER.debug("Private zone found %s", str( + private_hosted_zone_name) + lineno()) + + private_hosted_zone_id = get_zone_id(route53, private_hosted_zone_name) + LOGGER.debug("Private_hosted_zone_id: %s", str( + private_hosted_zone_id) + lineno()) + private_hosted_zone_properties = get_hosted_zone_properties( + route53, + private_hosted_zone_id + ) + + LOGGER.debug("private_hosted_zone_properties:" + " %s", str(private_hosted_zone_properties) + lineno()) + + has_dhcp_dns_zone_associated_vpc = True LOGGER.debug("iterating through tags %s", lineno()) - # Loop through the instance's tags, looking for the zone and - # cname tags. If either of these tags exist, check - # to make sure that the name is valid. If it is and - # if there's a matching zone in DNS, create A and PTR records. + + # Loop through the instance's tags + # validate the tag value and set certain boolean values + has_valid_zone_tag = False + has_valid_cname_tag = False + has_valid_hostname_tag = False + has_valid_Name_tag = False + for tag in tags: LOGGER.debug("#### tag: %s", str(tag) + lineno()) - if TAGKEY_ZONE in tag.get('Key', {}).lstrip().upper(): - # Simple check to make sure the hostname is valid - if is_valid_hostname(tag.get('Value')): - LOGGER.debug("hostname is valid %s", lineno()) - LOGGER.debug("checking if value in private:" - " %s", str(list(private_hosted_zone_collection)) + lineno()) - # awspeter - commneted out public dns - # LOGGER.debug("checking if value in public:" - # " %s", str(list(public_hosted_zones_collection)) + lineno()) - - if tag.get('Value').lstrip().lower() in private_hosted_zone_collection: - LOGGER.debug("Private zone found: %s", - str(tag.get('Value')) + lineno()) - private_hosted_zone_name = tag.get('Value').lstrip().lower() - LOGGER.debug("private_zone_name: %s", str( - private_hosted_zone_name) + lineno()) - private_hosted_zone_id = get_zone_id( - route53, private_hosted_zone_name) - LOGGER.debug("private_hosted_zone_id:" - " %s", str(private_hosted_zone_id) + lineno()) - private_hosted_zone_properties = get_hosted_zone_properties( - route53, private_hosted_zone_id) - LOGGER.debug("private_hosted_zone_properties:" - " %s", str(private_hosted_zone_properties) + lineno()) - fqdn = private_host_name + '.' + private_hosted_zone_name - if state == 'running': - found_vpc_id = False - if 'VPCs' in private_hosted_zone_properties: - for vpc in private_hosted_zone_properties['VPCs']: - if vpc['VPCId'] == vpc_id: - found_vpc_id = True - if found_vpc_id: - LOGGER.info("Private hosted zone %s is associated with VPC %s %s", - private_hosted_zone_id, vpc_id, lineno()) - else: - LOGGER.info("Private hosted zone %s is NOT associated with VPC %s %s", - private_hosted_zone_id, vpc_id, lineno()) - # LOGGER.info("Associating zone %s with VPC %s %s", private_hosted_zone_id, vpc_id, lineno()) - # try: - # associate_zone(route53, private_hosted_zone_id, region, vpc_id) - # except BaseException as err: - # LOGGER.info('You cannot create an association with a VPC with an overlapping subdomain.\n', err) - # exit() - try: - if found_vpc_id: - create_resource_record( - route53, - private_hosted_zone_id, - private_host_name, - private_hosted_zone_name, - 'A', - private_ip - ) - - LOGGER.debug( - "appending to caller response %s", lineno()) - - caller_response.append('Created A record in zone id: ' + - str(private_hosted_zone_id) + - ' for hosted zone ' + - str(private_host_name) + '.' + - str(private_hosted_zone_name) + - ' with value: ' + - str(private_ip)) - - if reverse_zone_associated: - create_resource_record( - route53, - reverse_lookup_zone_id, - reversed_ip_address, - 'in-addr.arpa', - 'PTR', - fqdn - # private_dns_name - ) - - caller_response.append('Created PTR record in zone id: ' + - str(reverse_lookup_zone_id) + - ' for hosted zone ' + - str(reversed_ip_address) + - 'in-addr.arpa with value: ' + - str(fqdn)) -# str(private_dns_name)) - - except BaseException as err: - LOGGER.debug("%s", str(err) + lineno()) - else: - try: - delete_resource_record( - route53, - private_hosted_zone_id, - private_host_name, - private_hosted_zone_name, - 'A', - private_ip - ) - - caller_response.append('Deleted A record in zone id: ' + - str(private_hosted_zone_id) + - ' for hosted zone ' + - str(private_host_name) + '.' + - str(private_hosted_zone_name) + - ' with value: ' + - str(private_ip)) - - delete_resource_record( - route53, - reverse_lookup_zone_id, - reversed_ip_address, - 'in-addr.arpa', - 'PTR', - fqdn - # private_dns_name - ) - - caller_response.append('Deleted PTR record in zone id: ' + - str(reverse_lookup_zone_id) + - ' for hosted zone ' + - str(reversed_ip_address) + '.' + - str(private_dns_name) + - ' with value: ' + - str(fqdn)) -# str(private_dns_name)) - - except BaseException as err: - LOGGER.debug("%s", str(err) + lineno()) - # create PTR record - # awspeter - commneted out public dns - # elif tag.get('Value').lstrip().lower() in public_hosted_zones_collection: - # LOGGER.debug("Public zone found %s", tag.get('Value') + lineno()) - # public_hosted_zone_name = tag.get('Value').lstrip().lower() - - # public_hosted_zone_id = get_zone_id( - # route53, - # public_hosted_zone_name, - # private_zone=False - # ) - # # create A record in public zone - # if state == 'running': - # try: - # create_resource_record( - # route53, - # public_hosted_zone_id, - # cname_prefix, - # public_hosted_zone_name, - # 'A', - # public_ip - # ) - # caller_response.append('Created A record in zone id: ' + - # str(public_hosted_zone_id) + - # ' for hosted zone ' + - # str(cname_prefix) + '.' + - # str(public_hosted_zone_name) + - # ' with value: ' + - # str(public_ip)) - # except BaseException as err: - # LOGGER.debug("%s", str(err) + lineno()) - # else: - # try: - # delete_resource_record( - # route53, - # public_hosted_zone_id, - # cname_prefix, - # public_hosted_zone_name, - # 'A', - # public_ip - # ) - # caller_response.append('Deleted A record in zone id: ' + - # str(public_hosted_zone_id) + - # ' for hosted zone ' + - # str(cname_prefix) + '.' + - # str(public_hosted_zone_name) + - # ' with value: ' + - # str(public_ip)) - # except BaseException as err: - # LOGGER.debug("%s", str(err) + lineno()) - else: - LOGGER.info("No matching zone found for %s", tag.get('Value')) + if tag.get('Key').lstrip().lower() == TAGKEY_ZONE.lower(): + LOGGER.debug("Zone Tag key: %s", tag.get('Key') + lineno()) + + custom_zone_name = tag.get('Value').lstrip().lower() + + if custom_zone_name[-1] != '.': # add a trailing period if it does not have it. + custom_zone_name = custom_zone_name + '.' + # check if the zone is PHZ VPC associated + if custom_zone_name in private_hosted_zone_collection: + LOGGER.debug("Private zone found: %s", + str(custom_zone_name) + lineno()) + + zone_tag_hosted_zone_name = custom_zone_name + LOGGER.debug("zone_tag_hosted_zone_name: %s", str( + zone_tag_hosted_zone_name) + lineno()) + + has_valid_zone_tag = True else: - LOGGER.info("%s is not a valid host name %s", - tag.get('Value'), lineno()) - # Consider making this an elif CNAME - else: - LOGGER.debug("The tag \'%s\' is not a zone tag %s", - str(tag.get('Key')), lineno()) + LOGGER.debug("Private zone NOT found: %s", + str(custom_zone_name) + lineno()) - if TAGKEY_CNAME.upper() in tag.get('Key', {}).lstrip().upper(): + elif tag.get('Key').lstrip().lower() == TAGKEY_CNAME.lower(): + LOGGER.debug("CNAME Tag key: %s", tag.get('Key') + lineno()) - # Simple hostname check if is_valid_hostname(tag.get('Value')): - + LOGGER.debug("CNAME hostname of %s is valid %s", str(tag.get('Value')), lineno()) + # convert the cname value to lower case and strip whitespace and newline characters icname = tag.get('Value').lstrip().lower() + if icname[-1] != '.': # add a trailing period if it does not have it. + icname = icname + '.' + LOGGER.debug("icname: %s", str(icname) + lineno()) # Gets the prefix for the cname cname_host_name = icname.split('.')[0] @@ -594,358 +413,244 @@ def lambda_handler( cname_domain_suffix_id = get_zone_id(route53, cname_domain_suffix) LOGGER.debug("cname_domain_suffix_id: %s", str(cname_domain_suffix_id)) - # Iterate of the private hosted zones - LOGGER.debug("Iterating over private hosted zones %s", lineno()) - for cname_private_hosted_zone in private_hosted_zone_collection: - - LOGGER.debug("cname for private hosted zone in private hosted zone collection: %s", str( - cname_private_hosted_zone) + lineno()) - - cname_private_hosted_zone_id = get_zone_id( - route53, cname_private_hosted_zone) - LOGGER.debug("cname_private_hosted_zone_id:" - " %s", str(cname_private_hosted_zone_id) + lineno()) - LOGGER.debug("cname_domain_suffix_id:" - " %s", str(cname_domain_suffix_id) + lineno()) - - if cname_domain_suffix_id == cname_private_hosted_zone_id: - LOGGER.debug("cname_domain_suffix_id:" - " %s", str(cname_domain_suffix_id) + lineno()) - - if cname.endswith(cname_private_hosted_zone): - LOGGER.debug("cname ends with" - " %s", str(cname_private_hosted_zone) + lineno()) - - # create CNAME record in private zone - if state == 'running': - try: - LOGGER.debug( - "creating resource record %s", lineno()) - LOGGER.debug("private_dns_name:" - " %s", str(private_dns_name) + lineno()) - create_resource_record( - route53, - cname_private_hosted_zone_id, - cname_host_name, - cname_private_hosted_zone, - 'CNAME', - private_dns_name - ) - - caller_response.append('Created CNAME record in zone id: ' + - str(cname_private_hosted_zone_id) + - ' for hosted zone ' + - str(cname_host_name) + '.' + - str(cname_private_hosted_zone) + - ' with value: ' + - str(private_dns_name)) - - except BaseException as err: - LOGGER.debug("%s", str(err) + lineno()) - else: - try: - LOGGER.debug( - "deleting resource record %s", lineno()) - delete_resource_record( - route53, - cname_private_hosted_zone_id, - cname_host_name, - cname_private_hosted_zone, - 'CNAME', - private_dns_name - ) - - caller_response.append('Deleted CNAME record in zone id: ' + - str(cname_private_hosted_zone_id) + - ' for hosted zone ' + - str(cname_host_name) + '.' + - str(cname_private_hosted_zone) + - ' with value: ' + - str(private_dns_name)) - - except BaseException as err: - LOGGER.debug("%s", str(err) + lineno()) - # awspeter - commented out public record - # # Only do public if there is public ip on instance - # if public_dns_name: - # # Iterate over the public hosted zones - # LOGGER.debug("Iterating over public hosted zones %s", lineno()) - # for cname_public_hosted_zone in public_hosted_zones_collection: - # LOGGER.debug("cname in public hosted zone:" - # " %s", str(cname_public_hosted_zone) + lineno()) - # LOGGER.debug("cname is: %s", str(cname) + lineno()) - # if cname.endswith(cname_public_hosted_zone): - # cname_public_hosted_zone_id = get_zone_id( - # route53, - # cname_public_hosted_zone, - # False - # ) - # LOGGER.debug("cname_public_hosted_zone_id:" - # " %s", str(cname_public_hosted_zone_id) + lineno()) - - # # create CNAME record in public zone - # if state == 'running': - # try: - # create_resource_record( - # route53, - # cname_public_hosted_zone_id, - # cname_host_name, - # cname_public_hosted_zone, - # 'CNAME', - # public_dns_name - # ) - - # caller_response.append('Created CNAME record in zone id: ' + - # str(cname_public_hosted_zone_id) + - # ' for hosted zone ' + - # str(cname_host_name) + '.' + - # str(cname_public_hosted_zone) + - # ' with value: ' + - # str(public_dns_name)) - - # except BaseException as err: - # LOGGER.debug("%s", str(err) + lineno()) - # else: - # try: - # delete_resource_record( - # route53, - # cname_public_hosted_zone_id, - # cname_host_name, - # cname_public_hosted_zone, - # 'CNAME', - # public_dns_name - # ) - - # caller_response.append('Deleted CNAME record in zone id: ' + - # str(cname_public_hosted_zone_id) + - # ' for hosted zone ' + - # str(cname_host_name) + '.' + - # str(cname_public_hosted_zone) + - # ' with value: ' + - # str(public_dns_name)) - - # except BaseException as err: - # LOGGER.debug("%s", str(err) + lineno()) - # Is there a DHCP option set? - # Get DHCP option set configuration - LOGGER.debug("\n#############\nIterate over DHCP option sets %s\n", lineno()) + # check if the zone is PHZ VPC associated + if cname_domain_suffix.lower() in private_hosted_zone_collection: + LOGGER.debug("Private zone found: %s", + str(tag.get('Value')) + lineno()) - try: - LOGGER.debug("trying to get dhcp option set id %s", lineno()) - dhcp_options_id = get_dhcp_option_set_id_for_vpc(compute, vpc_id) - LOGGER.debug("dhcp_options_id: %s", str(dhcp_options_id) + lineno()) - dhcp_configurations = get_dhcp_configurations(compute, dhcp_options_id) - LOGGER.debug("dhcp_configurations: %s", str(get_dhcp_configurations) + lineno()) + has_valid_cname_tag = True + else: + LOGGER.debug("cname domain is not associated with vpc: %s", + cname_domain_suffix + lineno()) - except BaseException as err: - LOGGER.info("No DHCP option set assigned to this VPC %s\n", str(err) + lineno()) - exit() + elif tag.get('Key').lstrip().lower() == TAGKEY_HOSTNAME.lower(): + LOGGER.debug("Custom Hostname Tag key: %s", tag.get('Key') + lineno()) - # Look to see whether there's a DHCP option set assigned to - # the VPC. If there is, use the value of the domain name - # to create resource records in the appropriate Route 53 - # private hosted zone. This will also check to see whether - # there's an association between the instance's VPC and - # the private hosted zone. If there isn't, it will create it. - for configuration in dhcp_configurations: + if is_valid_hostname(tag.get('Value')): + LOGGER.debug("Custom hostname of %s is valid %s", + str(tag.get('Value')), lineno()) + # convert the hostname value to lower case and strip whitespace and newline characters + hostname = tag.get('Value').lstrip().lower() - LOGGER.debug("configuration: %s", str(configuration) + lineno()) - LOGGER.debug("private hosted zones: %s", str( - private_hosted_zone_collection) + lineno()) + LOGGER.debug("hostname: %s", str(hostname) + lineno()) + # Gets the prefix for the custom hostname + custom_host_name = hostname.split('.')[0] + LOGGER.debug("custom_host_name: %s", str(custom_host_name) + lineno()) - if configuration in private_hosted_zone_collection: - private_hosted_zone_name = configuration - LOGGER.debug("Private zone found %s", str( - private_hosted_zone_name) + lineno()) + has_valid_hostname_tag = True + else: + LOGGER.debug("Custom hostname of %s is invalid %s", + str(tag.get('Value')), lineno()) - private_hosted_zone_id = get_zone_id(route53, private_hosted_zone_name) - LOGGER.debug("Private_hosted_zone_id: %s", str( - private_hosted_zone_id) + lineno()) - private_hosted_zone_properties = get_hosted_zone_properties( + elif tag.get('Key') == 'Name': + LOGGER.debug("Name Tag key: %s", tag.get('Key') + lineno()) + + if is_valid_hostname(tag.get('Value')): + LOGGER.debug("Name of %s is valid %s", + str(tag.get('Value')), lineno()) + # convert the hostname value to lower case and strip whitespace and newline characters + name_value = tag.get('Value').lstrip().lower() + + if name_value[-1] != '.': # add a trailing period if it does not have it. + name_value = name_value + '.' + + LOGGER.debug("Name: %s", str(name_value) + lineno()) + + # Gets the host and the zone name (split up based) + name_host = name_value.split('.')[0] + LOGGER.debug("name_host: %s", str( + name_host) + lineno()) + name_domain_suffix = name_value[name_value.find('.') + 1:] + LOGGER.debug("name_domain_suffix: %s", str( + name_domain_suffix) + lineno()) + + # check if the zone name == dhcp zone + if name_domain_suffix == private_hosted_zone_name: + LOGGER.debug("name_domain_suffix is the same as DHCP dns: %s", str(name_domain_suffix) + lineno()) + has_valid_Name_tag = True + else: + LOGGER.debug("name_domain_suffix is NOT the same as DHCP dns: %s", str(name_domain_suffix) + lineno()) + else: + LOGGER.debug("Name of %s is invalid %s", + str(tag.get('Value')), lineno()) + else: + LOGGER.debug("Skipping Tag key: %s", tag.get('Key') + lineno()) + + # determine correct A/PTR record to be created based upon the boolean values from the tags above + if has_valid_hostname_tag and has_valid_zone_tag: + LOGGER.info("custom hostname tag and custom zone tag valid.") + final_private_hostname = custom_host_name + final_hosted_zone_name = zone_tag_hosted_zone_name + elif has_valid_hostname_tag and not (has_valid_zone_tag) and has_dhcp_dns_zone_associated_vpc: #3 + LOGGER.info("custom hostname valid only.") + final_private_hostname = custom_host_name + final_hosted_zone_name = private_hosted_zone_name + elif has_valid_Name_tag and has_dhcp_dns_zone_associated_vpc: + LOGGER.info("Name tag valid.") + final_private_hostname = name_host + final_hosted_zone_name = private_hosted_zone_name + elif has_valid_zone_tag and not (has_valid_hostname_tag): + LOGGER.info("custom zone tag valid.") + final_private_hostname = private_host_name + final_hosted_zone_name = zone_tag_hosted_zone_name + elif has_dhcp_dns_zone_associated_vpc: + LOGGER.info("no custom tags - use default.") + final_private_hostname = private_host_name + final_hosted_zone_name = private_hosted_zone_name + else: # none of the use-casem and no suitable zone to create the A record + LOGGER.info("No DHCP Associated for VPC and no custom tags. Exiting Script") + # nothing to do, exit out script + exit(-1) + + # put together the FQDN of the dns name... + final_private_dns_name = final_private_hostname + '.' + final_hosted_zone_name + LOGGER.info("final hostname for A and PTR record: %s", str(final_private_dns_name) + lineno()) + + # Get the PHZ ID for the Zone + final_hosted_zone_id = get_zone_id( + route53, final_hosted_zone_name) + LOGGER.debug("private_hosted_zone_id:" + " %s", str(final_hosted_zone_id) + lineno()) + + # Create OR Delete the A / PTR Record + if state == 'running': + # create the records + try: + LOGGER.debug("Creating resource records %s", lineno()) + create_resource_record( route53, - private_hosted_zone_id + final_hosted_zone_id, + final_private_hostname, + final_hosted_zone_name, + 'A', + private_ip ) - LOGGER.debug("private_hosted_zone_properties:" - " %s", str(private_hosted_zone_properties) + lineno()) + caller_response.append('Created A record in zone id: ' + + str(final_hosted_zone_id) + + ' for hosted zone ' + + str(final_private_hostname) + '.' + + str(final_hosted_zone_name) + + ' with value: ' + + str(private_ip)) + + if reverse_zone_associated: + create_resource_record( + route53, + reverse_lookup_zone_id, + reversed_ip_address, + 'in-addr.arpa', + 'PTR', + final_private_dns_name + ) + + caller_response.append('Created PTR record in zone id: ' + + str(reverse_lookup_zone_id) + + ' for hosted zone ' + + str(reversed_ip_address) + + 'in-addr.arpa with value: ' + + str(final_private_dns_name)) + + except BaseException as err: + LOGGER.info("unexpected error. %s\n", str(err) + lineno()) - # create A records and PTR records - fqdn = private_host_name + '.' + private_hosted_zone_name - if state == 'running': - if vpc_id in map(lambda x: x['VPCId'], private_hosted_zone_properties['VPCs']): - vpc_associated = True - LOGGER.info("Private hosted zone %s is associated with VPC %s %s", - private_hosted_zone_id, vpc_id, lineno()) - else: - vpc_associated = False - LOGGER.info("Private hosted zone %s is NOT associated with VPC %s %s", - private_hosted_zone_id, vpc_id, lineno()) - exit() - # LOGGER.info("Associating zone %s with VPC" - # " %s %s", private_hosted_zone_id, vpc_id, lineno()) - # try: - # associate_zone(route53, private_hosted_zone_id, region, vpc_id) - # except BaseException as err: - # LOGGER.info("You cannot create an association with a VPC with an overlapping subdomain. %s\n", str(err)) - # exit() - try: - - if not has_custom_hostname: - if vpc_associated: - LOGGER.debug("Creating resource records %s", lineno()) - create_resource_record( - route53, - private_hosted_zone_id, - private_host_name, - private_hosted_zone_name, - 'A', - private_ip - ) - - caller_response.append('Created A record in zone id: ' + - str(private_hosted_zone_id) + - ' for hosted zone ' + - str(private_host_name) + '.' + - str(private_hosted_zone_name) + - ' with value: ' + - str(private_ip)) - else: - LOGGER.debug( - "No forward zone associated with VPC - skipping creating resource records %s", lineno()) - if reverse_zone_associated: - create_resource_record( - route53, - reverse_lookup_zone_id, - reversed_ip_address, - 'in-addr.arpa', - 'PTR', - fqdn - # private_dns_name - ) - - caller_response.append('Created PTR record in zone id: ' + - str(reverse_lookup_zone_id) + - ' for hosted zone ' + - str(reversed_ip_address) + - 'in-addr.arpa with value: ' + - str(fqdn)) -# str(private_dns_name)) - else: - LOGGER.debug( - "No reverse zone associated with VPC - skipping creating resource records %s", lineno()) - - else: - if vpc_associated: - LOGGER.debug("Creating resource records %s", lineno()) - create_resource_record( - route53, - private_hosted_zone_id, - cname_prefix, # awspeter - that should be private host - private_hosted_zone_name, - 'A', - private_ip - ) - - caller_response.append('Created A record in zone id: ' + - str(private_hosted_zone_id) + ' for hosted zone ' + - str(cname_prefix) + '.' + - str(private_hosted_zone_name) + ' with value: ' + - str(private_ip)) - else: - LOGGER.debug( - "No forward zone associated with VPC - skipping creating resource records %s", lineno()) - if reverse_zone_associated: - create_resource_record( - route53, - reverse_lookup_zone_id, - reversed_ip_address, - 'in-addr.arpa', - 'PTR', - cname - ) - - caller_response.append('Created PTR record in zone id: ' + - str(reverse_lookup_zone_id) + - ' for hosted zone ' + - str(reversed_ip_address) + - 'in-addr.arpa with value: ' + - str(cname)) - else: - LOGGER.debug( - "No reverse zone associated with VPC - skipping creating resource records %s", lineno()) - - except BaseException as err: - LOGGER.info("unexpected error. %s\n", str(err) + lineno()) - else: + else: # not running so delete the records + try: + delete_resource_record( + route53, + final_hosted_zone_id, + final_private_hostname, + final_hosted_zone_name, + 'A', + private_ip + ) + + caller_response.append('Deleted A record in zone id: ' + + str(final_hosted_zone_id) + + ' for hosted zone ' + + str(final_private_hostname) + '.' + + str(final_hosted_zone_name) + + ' with value: ' + + str(private_ip)) - LOGGER.debug("Deleting resource records: %s", lineno()) - try: - if not has_custom_hostname: - delete_resource_record( - route53, - private_hosted_zone_id, - private_host_name, - private_hosted_zone_name, - 'A', - private_ip - ) - caller_response.append('Deleted A record in zone id: ' + - str(private_hosted_zone_id) + ' for hosted zone ' + - str(private_host_name) + '.' + - str(private_hosted_zone_name) + ' with value: ' + - str(private_ip)) - - delete_resource_record( - route53, - reverse_lookup_zone_id, - reversed_ip_address, - 'in-addr.arpa', - 'PTR', - fqdn - # private_dns_name - ) - caller_response.append('Deleted PTR record in zone id: ' + - str(reverse_lookup_zone_id) + - ' for hosted zone ' + - str(reversed_ip_address) + - 'in-addr.arpa with value: ' + - str(fqdn)) -# str(private_dns_name)) - else: - delete_resource_record( - route53, - private_hosted_zone_id, - cname_prefix, - private_hosted_zone_name, - 'A', - private_ip - ) - caller_response.append('Deleted A record in zone id: ' + - str(private_hosted_zone_id) + ' for hosted zone ' + - str(cname_prefix) + '.' + - str(private_hosted_zone_name) + ' with value: ' + - str(private_ip)) - delete_resource_record( - route53, - reverse_lookup_zone_id, - reversed_ip_address, - 'in-addr.arpa', - 'PTR', - cname - ) - - caller_response.append('Deleted PTR record in zone id: ' + - str(reverse_lookup_zone_id) + - ' for hosted zone ' + - str(reversed_ip_address) + - 'in-addr.arpa with value: ' + - str(cname)) - - except BaseException as err: - LOGGER.info("unexpected error. %s\n", str(err) + lineno()) + delete_resource_record( + route53, + reverse_lookup_zone_id, + reversed_ip_address, + 'in-addr.arpa', + 'PTR', + final_private_dns_name + ) + + caller_response.append('Deleted PTR record in zone id: ' + + str(reverse_lookup_zone_id) + + ' for hosted zone ' + + str(reversed_ip_address) + + str(private_dns_name) + + ' with value: ' + + str(final_private_dns_name)) + + except BaseException as err: + LOGGER.debug("%s", str(err) + lineno()) + + # Create the CNAME record only if it has passed the check + if has_valid_cname_tag: + LOGGER.debug("cname record is valid - creating CNAME record:" + " %s", str(cname_host_name) + "." + str(cname_domain_suffix) + lineno()) + + # create CNAME record in private zone + if state == 'running': + try: + LOGGER.debug("cname_host_name:" + " %s", str(cname_host_name) + lineno()) + LOGGER.debug("cname_domain_suffix:" + " %s", str(cname_domain_suffix) + lineno()) + LOGGER.debug("cname_domain_suffix_id:" + " %s", str(cname_domain_suffix_id) + lineno()) + + create_resource_record( + route53, + cname_domain_suffix_id, + cname_host_name, + cname_domain_suffix, + 'CNAME', + final_private_dns_name + ) + + caller_response.append('Created CNAME record in zone id: ' + + str(cname_domain_suffix_id) + + ' for hosted zone ' + + str(cname_host_name) + '.' + + str(cname_domain_suffix) + + ' with value: ' + + str(final_private_dns_name)) + + except BaseException as err: + LOGGER.debug("%s", str(err) + lineno()) else: - LOGGER.debug("No matching zone for %s", str(configuration) + lineno()) + try: + LOGGER.debug( + "deleting resource record %s", lineno()) + delete_resource_record( + route53, + cname_domain_suffix_id, + cname_host_name, + cname_domain_suffix, + 'CNAME', + final_private_dns_name + ) + + caller_response.append('Deleted CNAME record in zone id: ' + + str(cname_domain_suffix_id) + + ' for hosted zone ' + + str(cname_host_name) + '.' + + str(cname_domain_suffix) + + ' with value: ' + + str(final_private_dns_name)) + + except BaseException as err: + LOGGER.debug("%s", str(err) + lineno()) # Clean up DynamoDB after deleting records if state != 'running': @@ -959,35 +664,6 @@ def lambda_handler( return caller_response -def determine_tag_type(tags): - """ - Determine tag type - CNAME or ZONE - :param tags: - :return: - """ - cname = -1 - zone = -1 - has_custom_hostname = False - - for item in tags: - LOGGER.debug("item: %s", str(item) + lineno()) - - if item['Key'].lower() == TAGKEY_CNAME.lower(): - cname = 1 - elif item['Key'].lower() == TAGKEY_ZONE.lower(): - zone = 1 - elif item['Key'].lower() == TAGKEY_HOSTNAME.lower(): - has_custom_hostname = True - - if cname < 0 and zone < 0: - return [None, has_custom_hostname] - elif cname > 0 and zone < 0: - return ['cname_selected', has_custom_hostname] - elif cname < 0 and zone > 0: - return ['zone_selected', has_custom_hostname] - return 'invalid' - - def get_cname_from_tags(tags): """ Get the cname prefix from tags @@ -1137,44 +813,6 @@ def get_private_hosted_zone_collection(private_hosted_zones): except: LOGGER.info("unexpected error. %s\n", str(sys.exc_info()[0]) + lineno()) -# awspeter - commneted out public dns -# def get_public_hosted_zone_collection(public_hosted_zones): -# """ -# Get public hosted zone collection -# :param public_hosted_zones: -# :return: -# """ -# try: -# public_hosted_zone_collection = [] - -# for item in public_hosted_zones: -# LOGGER.debug("items: %s", str(item) + lineno()) -# public_hosted_zone_collection.append(item['Name']) - -# return public_hosted_zone_collection -# except: -# LOGGER.info("unexpected error. %s\n", str(sys.exc_info()[0]) + lineno()) - -# awspeter - commneted out public dns -# def get_public_hosted_zones(hosted_zones): -# """ -# Get public hosted zones -# :param hosted_zones: -# :return: -# """ -# try: -# public_hosted_zones = [] - -# for item in hosted_zones['HostedZones']: -# LOGGER.debug("item: %s", str(item) + lineno()) - -# if not item['Config']['PrivateZone']: -# public_hosted_zones.append(item) - -# return public_hosted_zones -# except: -# LOGGER.info("unexpected error. %s\n", str(sys.exc_info()[0]) + lineno()) - def get_private_hosted_zones(hosted_zones): """ @@ -1220,36 +858,6 @@ def get_dhcp_option_set_id_for_vpc(client, vpc_id): except ClientError as err: LOGGER.info("unexpected error. %s\n", str(err) + lineno()) -# def create_dynamodb_table(client, table_name): -# """ -# Create dynamodb table -# :param client: -# :param table_name: -# :return: -# """ -# try: -# return client.create_table( -# TableName=table_name, -# AttributeDefinitions=[ -# { -# 'AttributeName': 'InstanceId', -# 'AttributeType': 'S' -# }, -# ], -# KeySchema=[ -# { -# 'AttributeName': 'InstanceId', -# 'KeyType': 'HASH' -# }, -# ], -# ProvisionedThroughput={ -# 'ReadCapacityUnits': 4, -# 'WriteCapacityUnits': 4 -# } -# ) -# except ClientError as err: -# LOGGER.info("unexpected error. %s\n", str(err) + lineno()) - def get_dynamodb_table(client, table_name): """ @@ -1265,29 +873,6 @@ def get_dynamodb_table(client, table_name): except ClientError as err: LOGGER.info("unexpected error. %s\n", str(err) + lineno()) -# def create_table(client, table_name): -# """ -# Create dynamodb table -# :param client: -# :param table_name: -# :return: -# """ -# try: -# create_dynamodb_table(client, table_name) -# created = -1 -# while created < 0: -# table = get_dynamodb_table(client, table_name) - -# if table['Table']['TableStatus'] == 'ACTIVE': -# created = 1 -# else: -# time.sleep(15) - -# return True -# except: -# LOGGER.info("unexpected error. %s\n", str(sys.exc_info()[0]) + lineno()) - - def change_resource_recordset(client, zone_id, host_name, hosted_zone_name, record_type, value): """ Change resource recordset @@ -1508,9 +1093,10 @@ def get_dhcp_configurations(client, dhcp_options_id): dhcp_configurations = response['DhcpOptions'][0]['DhcpConfigurations'] LOGGER.debug("dhcp_configurations: %s", str(dhcp_configurations) + lineno()) for configuration in dhcp_configurations: - for item in configuration['Values']: - LOGGER.debug("item: %s", str(item) + lineno()) - zone_names.append(str(item['Value']) + '.') + if configuration['Key']=='domain-name': # only if the key is domain-name + for item in configuration['Values']: + LOGGER.debug("item: %s", str(item) + lineno()) + zone_names.append(str(item['Value']) + '.') LOGGER.debug("zone name: %s", str(zone_names) + lineno()) return zone_names except: @@ -1569,36 +1155,6 @@ def get_reversed_domain_prefix(subnet_mask, private_ip): except: LOGGER.info("unexpected error. %s\n", str(sys.exc_info()[0]) + lineno()) -# def create_reverse_lookup_zone(client, instance, reversed_domain_prefix, region): -# """ -# Creates the reverse lookup zone. -# :param client: -# :param instance: -# :param reversed_domain_prefix: -# :param region: -# :return: -# """ -# try: -# LOGGER.debug('Creating reverse lookup zone %s in.addr.arpa.' -# ' %s', str(reversed_domain_prefix), lineno()) - -# if reversed_domain_prefix[-1] == ".": -# reversed_domain_prefix = reversed_domain_prefix[:-1] - -# return client.create_hosted_zone( -# Name=reversed_domain_prefix + '.in-addr.arpa.', -# VPC={ -# 'VPCRegion': region, -# 'VPCId': instance['Reservations'][0]['Instances'][0]['VpcId'] -# }, -# CallerReference=str(uuid.uuid1()), -# HostedZoneConfig={ -# 'Comment': 'Updated by Lambda DDNS' -# } -# ) -# except: -# LOGGER.info("unexpected error. %s\n", str(sys.exc_info()[0]) + lineno()) - def json_serial(obj): """ @@ -1633,27 +1189,6 @@ def remove_empty_from_dict(dictionary): except: LOGGER.info("unexpected error. %s\n", str(sys.exc_info()[0]) + lineno()) -# def associate_zone(client, hosted_zone_id, region, vpc_id): -# """ -# Associates private hosted zone with VPC -# :param client: -# :param hosted_zone_id: -# :param region: -# :param vpc_id: -# :return: -# """ -# try: -# return client.associate_vpc_with_hosted_zone( -# HostedZoneId=hosted_zone_id, -# VPC={ -# 'VPCRegion': region, -# 'VPCId': vpc_id -# }, -# Comment='Updated by Lambda DDNS' -# ) -# except: -# LOGGER.info("unexpected error. %s\n", str(sys.exc_info()[0]) + lineno()) - def is_dns_hostnames_enabled(client, vpc_id): """ @@ -1729,21 +1264,3 @@ def get_subnet_cidr_block(client, subnet_id): return response['Subnets'][0]['CidrBlock'] except: LOGGER.info("unexpected error. %s\n", str(sys.exc_info()[0]) + lineno()) - -# def publish_to_sns(client, account, region, message): -# """ -# Publish a simple message to the specified SNS topic -# :param client: -# :param account: -# :param region: -# :param message: -# :return: -# """ -# LOGGER.debug("SNS message: %s ", str(message)+lineno()) -# try: -# client.publish( -# TopicArn='arn:aws:sns:' + str(region) + ':' + str(account) + ':DDNSAlerts', -# Message=str(message) -# ) -# except ClientError as err: -# LOGGER.debug("Unexpected error: %s", str(err)+lineno())