diff --git a/code/ddns-lambda.py b/code/ddns-lambda.py old mode 100644 new mode 100755 index f0f9acf..d9f46af --- a/code/ddns-lambda.py +++ b/code/ddns-lambda.py @@ -1,1661 +1,1661 @@ -# 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 -""" -import json -import sys -import datetime -import random -import logging -import re -import uuid -import time -import inspect -import boto3 -import os -from botocore.exceptions import ClientError - -# Setting Global Variables -LOGGER = logging.getLogger() -ACCOUNT = None -REGION = None - -# 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['SleepTime']) -DDBNAME = os.environ['DynamoDBName'] -TAGKEY_CNAME = os.environ['TagKeyCname'] -TAGKEY_ZONE = os.environ['TagKeyZone'] -TAGKEY_HOSTNAME = os.environ['TagKeyHostName'] - -print('Loading function ' + datetime.datetime.now().time().isoformat()) - -def lineno(): # pragma: no cover - """ - Returns the current line number in our script - :return: - """ - 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 - :return: - """ - try: - return boto3.client('route53') - except ClientError as err: - print("Unexpected error: %s" % err) - -def get_ec2_client(): - """ - Get ec2 client - :return: - """ - try: - return boto3.client('ec2') - except ClientError as err: - print("Unexpected error: %s" % err) - -def get_dynamodb_client(): - """ - Get dynamodb client - :return: - """ - try: - return boto3.client('dynamodb') - except ClientError as err: - print("Unexpected error: %s" % err) - -def lambda_handler( - event, - context, - dynamodb_client=get_dynamodb_client(), - compute=get_ec2_client(), - route53=get_route53_client() -): - """ - Check to see whether a DynamoDB table already exists. If not, create it. - This table is used to keep a record of instances that have been created - along with their attributes. This is necessary because when you terminate an instance - its attributes are no longer available, so they have to be fetched from the table. - :param event: - :param context: - :param dynamodb_client: - :param compute: - :param route53: - :param sns_client: - :return: - """ - 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 - tables = list_tables(dynamodb_client) - - LOGGER.info("tables: %s", str(tables)) - if DDBNAME in tables['TableNames']: - LOGGER.info('DynamoDB table already exists') - 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 - state = event['detail']['state'] - LOGGER.debug("instance state: %s", str(state) + lineno()) - - # 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 - if state == 'running': - LOGGER.debug("sleeping for 60 seconds %s", lineno()) - - if "pytest" in sys.modules: - # called from within a test run - time.sleep(1) - else: - # called "normally" - time.sleep(SLEEPTIME) - - # Get instance information - instance = get_instances(compute, instance_id) - # Remove response metadata from the response - if 'ResponseMetadata' in instance: - instance.pop('ResponseMetadata') - # Remove null values from the response. You cannot save a dict/JSON - # document in DynamoDB if it contains null values - LOGGER.debug("instance: %s", str(instance) + lineno()) - instance = remove_empty_from_dict(instance) - instance_dump = json.dumps(instance, default=json_serial) - instance_attributes = json.loads(instance_dump) - LOGGER.debug("instance_attributes: %s", str(instance_attributes) + lineno()) - LOGGER.debug("trying to put instance information in " - "dynamo table %s", str(instance_attributes) + lineno()) - put_item_in_dynamodb_table(dynamodb_client, DDBNAME, instance_id, instance_attributes) - LOGGER.debug("done putting item in dynamo table %s", lineno()) - else: - # Fetch item from DynamoDB - LOGGER.debug("Fetching instance information from dynamodb %s", lineno()) - instance = get_item_from_dynamodb_table(dynamodb_client, DDBNAME, instance_id) - LOGGER.debug("instance: %s", str(instance) + lineno()) - - # Get the instance tags and reorder them because we want a zone created before CNAME - try: - tags = instance['Reservations'][0]['Instances'][0]['Tags'] - except: - tags = [] - - LOGGER.debug("tags are: %s", str(tags) + lineno()) - - # tag_type = determine_tag_type(tags) - tag_type = determine_tag_type(tags)[0] # changed to return a list, so read the 1st value - has_custom_hostname = determine_tag_type(tags)[1] # if hostname is found in the - - LOGGER.debug("tag type %s", tag_type + lineno()) - LOGGER.debug("has custom hostname tag %s", 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()) - if instance and 'Reservations' in instance: - LOGGER.debug("reservations: %s", str(instance['Reservations']) + lineno()) - LOGGER.debug("reservations: %s", str(instance['Reservations'][0]) + lineno()) - LOGGER.debug("reservations: %s", str(instance['Reservations'][0]['Instances']) + lineno()) - LOGGER.debug("reservations:" - " %s", str(instance['Reservations'][0]['Instances'][0]) + lineno()) - - private_ip = instance['Reservations'][0]['Instances'][0]['PrivateIpAddress'] - private_dns_name = instance['Reservations'][0]['Instances'][0]['PrivateDnsName'] - private_host_name = private_dns_name.split('.')[0] - - LOGGER.debug("private ip: %s", str(private_ip) + lineno()) - 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()) - cidr_block = get_subnet_cidr_block(compute, subnet_id) - LOGGER.debug("cidr_block: %s", str(cidr_block) + lineno()) - subnet_mask = int(cidr_block.split('/')[-1]) - LOGGER.debug("subnet_mask: %s", str(subnet_mask) + lineno()) - reversed_ip_address = reverse_list(private_ip) - - reversed_domain_prefix = get_reversed_domain_prefix(subnet_mask, private_ip) - reversed_domain_prefix = reverse_list(reversed_domain_prefix) - LOGGER.debug("reversed_domain_prefix is: %s", str(reversed_domain_prefix) + lineno()) - # Set the reverse lookup zone - reversed_lookup_zone = reversed_domain_prefix + 'in-addr.arpa.' - LOGGER.info("The reverse lookup zone for this instance is: %s", str(reversed_lookup_zone)) - - # Get VPC id - vpc_id = instance['Reservations'][0]['Instances'][0]['VpcId'] - - # Are DNS Hostnames and DNS Support enabled? - if is_dns_hostnames_enabled(compute, vpc_id): - LOGGER.debug("DNS hostnames enabled for %s", str(vpc_id) + lineno()) - else: - LOGGER.debug("DNS hostnames disabled for %s. You have to enable DNS hostnames to use Route 53 private hosted zones. %s", vpc_id, lineno()) - if is_dns_support_enabled(compute, vpc_id): - LOGGER.debug("DNS support enabled for %s", str(vpc_id) + lineno()) - else: - LOGGER.debug("DNS support disabled for %s. You have to enabled DNS support to use Route 53 private hosted zones. %s", str(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()) - private_hosted_zones = get_private_hosted_zones(hosted_zones) - LOGGER.debug("private_hosted_zones: %s", str(list(private_hosted_zones)) + lineno()) - private_hosted_zone_collection = get_private_hosted_zone_collection(private_hosted_zones) - 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. - LOGGER.info("reversed_lookup_zone: %s", str(reversed_lookup_zone) + lineno()) - reverse_zone = None - for record in hosted_zones['HostedZones']: - LOGGER.debug("record name: %s", str(record['Name']) + lineno()) - if record['Name'] == reversed_lookup_zone: - reverse_zone = record['Name'] - break - if reverse_zone: - LOGGER.debug("Reverse lookup zone found: %s", str(reversed_lookup_zone) + lineno()) - reverse_lookup_zone_id = get_zone_id(route53, reversed_lookup_zone) - LOGGER.debug("reverse_lookup_zone_id: %s", str(reverse_lookup_zone_id) + lineno()) - - reverse_hosted_zone_properties = get_hosted_zone_properties(route53, reverse_lookup_zone_id) - LOGGER.debug("reverse_hosted_zone_properties:" - " %s", str(reverse_hosted_zone_properties) + lineno()) - - if vpc_id in map(lambda x: x['VPCId'], reverse_hosted_zone_properties['VPCs']): - LOGGER.info("Reverse lookup zone %s is associated with VPC %s %s", reverse_lookup_zone_id, vpc_id, lineno()) - reverse_zone_associated = True - else: - LOGGER.info("Reverse lookup zone %s is NOT associated with VPC %s %s", 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() - # ) - - 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. - 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()) - 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', - 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(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', - 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(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')) - 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()) - - if TAGKEY_CNAME.upper() in tag.get('Key', {}).lstrip().upper(): - - # 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() - - LOGGER.debug("icname: %s", str(icname) + lineno()) - # Gets the prefix for the cname - cname_host_name = icname.split('.')[0] - LOGGER.debug("cname_host_name: %s", str(cname_host_name) + lineno()) - - # Gets suffix - cname_domain_suffix = icname[icname.find('.') + 1:] - LOGGER.debug("cname_domain_suffix: %s", str(cname_domain_suffix) + lineno()) - - # Try and find the hosted zone with the cname suffix - 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()) - - 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. 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: - - 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()) - - # create A records and PTR records - 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', - 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(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: - - 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', - 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(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()) - else: - LOGGER.debug("No matching zone for %s", str(configuration) + lineno()) - - # Clean up DynamoDB after deleting records - if state != 'running': - delete_item_from_dynamodb_table(dynamodb_client, DDBNAME, instance_id) - - caller_response.insert(0, 'Successfully removed recordsets') - return caller_response - - caller_response.insert(0, 'Successfully created recordsets') - - 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 - :param tags: - :return: - """ - - try: - for tag in tags: - LOGGER.debug("tag: %s", str(tag)) - - if TAGKEY_CNAME.upper() in tag.get('Key', {}).lstrip().upper(): - cname = tag.get('Value').lstrip().lower() - - return cname - return None - except: - LOGGER.info("unexpected error. %s\n", str(sys.exc_info()[0])) - -def get_instances(client, instance_id): - """ - Get ec2 instance information - :return: - """ - try: - return client.describe_instances(InstanceIds=[instance_id]) - except ClientError as err: - LOGGER.info("unexpected error. %s\n", str(err) + lineno()) - -def list_hosted_zones(client): - """ - Get route53 hosted zones - :param client: - :return: - """ - try: - return client.list_hosted_zones() - except ClientError as err: - LOGGER.info("unexpected error. %s\n", str(err) + lineno()) - -def list_tables(client): - """ - List the dynamodb tables - :param client: - :return: - """ - try: - return client.list_tables() - except ClientError as err: - LOGGER.info("unexpected error. %s\n", str(err) + lineno()) - -def delete_item_from_dynamodb_table(client, table, instance_id): - """ - Delete the item from dynamodb table - :param client: - :param table: - :param instance_id: - :return: - """ - try: - return client.delete_item( - TableName=table, - Key={ - 'InstanceId': {'S': instance_id} - }) - except ClientError as err: - LOGGER.info("unexpected error. %s\n", str(err) + lineno()) - -def put_item_in_dynamodb_table(client, table, instance_id, instance_attributes): - """ - Put item in dynamodb table - :param client: - :param table: - :param instance_id: - :param instance_attributes: - :return: - """ - try: - LOGGER.debug("attributes: %s", str(instance_attributes) + lineno()) - LOGGER.debug("putting attributes: %s", str(instance_attributes) + lineno()) - - return client.put_item( - TableName=str(table), - Item={ - 'InstanceId': {'S': instance_id}, - 'InstanceAttributes': {'S': str(instance_attributes)} - } - ) - except ClientError as err: - LOGGER.info("unexpected error. %s\n", str(err) + lineno()) - -def get_item_from_dynamodb_table(client, table, instance_id): - """ - Get item from dynamodb table - :param client: - :param table: - :param instance_id: - :return: - """ - try: - # Fetch item from DynamoDB - item = client.get_item( - TableName=table, - Key={ - 'InstanceId': { - 'S': instance_id - } - }, - AttributesToGet=[ - 'InstanceAttributes' - ] - ) - - if 'Item' in item: - LOGGER.debug("returned item:" - " %s", str(item['Item']['InstanceAttributes']['S']) + lineno()) - item = item['Item']['InstanceAttributes']['S'].replace("'", '"') - item = item.replace(" True,", ' "True",') - item = item.replace(" False,", ' "False",') - LOGGER.debug("item: %s", str(item) + lineno()) - return json.loads(item) - return None - except ClientError as err: - LOGGER.info("unexpected error. %s\n", str(err) + lineno()) - -def get_private_hosted_zone_collection(private_hosted_zones): - """ - Get private hosted zone collection - :param private_hosted_zones: - :return: - """ - try: - private_hosted_zone_collection = [] - - for item in private_hosted_zones: - LOGGER.debug("item: %s", str(item) + lineno()) - private_hosted_zone_collection.append(item['Name']) - - return private_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_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): - """ - Get private hosted zones - :param hosted_zones: - :return: - """ - try: - private_hosted_zones = [] - - for item in hosted_zones['HostedZones']: - LOGGER.debug("item: %s", str(item) + lineno()) - - if item['Config']['PrivateZone']: - private_hosted_zones.append(item) - - return private_hosted_zones - except: - LOGGER.info("unexpected error. %s\n", str(sys.exc_info()[0]) + lineno()) - -def get_dhcp_option_set_id_for_vpc(client, vpc_id): - """ - Get the dhcp option set from vpc - :param client: - :param vpc_id: - :return: - """ - try: - option_sets = {} - - results = client.describe_vpcs() - - for item in results['Vpcs']: - - if 'DhcpOptionsId' in item: - option_sets[str(item['VpcId'])] = item['DhcpOptionsId'] - else: - option_sets[str(item['VpcId'])] = None - - return option_sets[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): - """ - Get the dynamodb table - :param client: - :param table_name: - :return: - """ - try: - return client.describe_table( - TableName=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 - :param client: - :param zone_id: - :param host_name: - :param hosted_zone_name: - :param value: - :return: - """ - try: - response = client.change_resource_record_sets( - HostedZoneId=zone_id, - ChangeBatch={ - "Comment": "Updated by Lambda DDNS", - "Changes": [ - { - "Action": "UPSERT", - "ResourceRecordSet": { - "Name": host_name + hosted_zone_name, - "Type": record_type, - "TTL": 60, - "ResourceRecords": [ - { - "Value": value - }, - ] - } - }, - ] - } - ) - - LOGGER.debug("response: %s", str(response) + lineno()) - return response - except ClientError as err: - LOGGER.debug("Error creating resource record: %s", str(err) + lineno()) - error_message = str(err) - - if "conflicts with other records" in error_message: - LOGGER.debug("Can not create dns record because of duplicates: %s", str(err) + lineno()) - return 'Duplicate resource record' - elif "conflicting RRSet" in error_message: - LOGGER.debug("Can not create dns record because of duplicates: %s", str(err) + lineno()) - return 'Conflicting resource record' - else: - LOGGER.info("unexpected error. %s\n", str(err) + lineno()) - return 'Unexpected error: ' + str(err) - -def create_resource_record(client, zone_id, host_name, hosted_zone_name, record_type, value): - """ - This function creates resource records in the hosted zone passed by the calling function. - :param client: - :param zone_id: - :param host_name: - :param hosted_zone_name: - :param record_type: - :param value: - :return: - """ - LOGGER.debug("Creating resource record: zone_id: %s host_name:" - " %s hosted_zone_name: %s record_type: %s value: %s %s", zone_id, - host_name, hosted_zone_name, record_type, value, lineno()) - try: - if host_name[-1] != '.': - host_name = host_name + '.' - - LOGGER.debug( - "Updating %s in zone %s%s to %s %s", record_type, host_name, - hosted_zone_name, value, lineno()) - - # To prevent rate throttling - time.sleep(1) - - response = change_resource_recordset( - client, - zone_id, - host_name, - hosted_zone_name, - record_type, - value - ) - - LOGGER.debug("response: %s", str(response) + lineno()) - return response - except ClientError as err: - LOGGER.debug("Error creating resource record: %s", str(err) + lineno()) - if 'is not permitted as it conflicts with other records ' \ - 'with the same DNS name in zone' in str(err): - LOGGER.debug("Can not create dns record because " - "of duplicates: %s", str(err) + lineno()) - -def delete_resource_record(client, zone_id, host_name, hosted_zone_name, record_type, value): - """ - This function deletes resource records from the hosted zone passed by the calling function. - :param client: - :param zone_id: - :param host_name: - :param hosted_zone_name: - :param record_type: - :param value: - :return: - """ - try: - LOGGER.debug("Deleting %s record %s in zone %s" - " %s", record_type, host_name, hosted_zone_name, lineno()) - if host_name[-1] != '.': - host_name = host_name + '.' - response = client.change_resource_record_sets( - HostedZoneId=zone_id, - ChangeBatch={ - "Comment": "Updated by Lambda DDNS", - "Changes": [ - { - "Action": "DELETE", - "ResourceRecordSet": { - "Name": host_name + hosted_zone_name, - "Type": record_type, - "TTL": 60, - "ResourceRecords": [ - { - "Value": value - }, - ] - } - } - ] - } - ) - - LOGGER.debug("delete record response: %s", str(response) + lineno()) - return response - - except ClientError as err: - if 'Not Found' in str(err): - LOGGER.debug("Record not found error: %s", str(err)+lineno()) - return - - if 'InvalidChangeBatch' in str(err) and 'it was not found' in str(err): - LOGGER.debug("Record not found error: %s", str(err)+lineno()) - return - - LOGGER.info("unexpected error. %s\n", str(err) + lineno()) - -def get_zone_id(client, zone_name, private_zone=True): - """ - This function returns the zone id for the zone name that's passed into the function. - :param client: - :param zone_name: - :return: - """ - try: - if zone_name[-1] != '.': - zone_name = zone_name + '.' - hosted_zones = list_hosted_zones(client) - - LOGGER.debug("zone name: %s", str(zone_name) + lineno()) - LOGGER.debug("hosted_zones: %s", str(hosted_zones) + lineno()) - zones = [] - for record in hosted_zones['HostedZones']: - LOGGER.debug("record: %s", str(record) + lineno()) - if record['Config']['PrivateZone'] == private_zone: - if record['Name'] == zone_name: - zones.append(record) - LOGGER.debug("zones: %s", str(zones) + lineno()) - - try: - zone_id_long = zones[0]['Id'] - LOGGER.debug("zone id: %s", str(zone_id_long) + lineno()) - zone_id = str.split(str(zone_id_long), '/')[2] - return zone_id - except: - return None - except: - LOGGER.info("unexpected error. %s\n", str(sys.exc_info()[0]) + lineno()) - -def is_valid_hostname(hostname): - """ - This function checks to see whether the hostname entered - into the zone and cname tags is a valid hostname. - :param hostname: - :return: - """ - try: - LOGGER.debug("determining if hostname is valid: %s", str(hostname) + lineno()) - if hostname is None or len(hostname) > 255: - return False - if hostname[-1] == ".": - hostname = hostname[:-1] - allowed = re.compile(r"(?!-)[A-Z\d-]{1,63}(? 0: - my_list.append(int(item)) - - LOGGER.debug("list1: %s", str(my_list) + lineno()) - LOGGER.debug("type: %s", str(type(my_list)) + lineno()) - - my_list.reverse() - reversed_list = '' - for item in my_list: - reversed_list = reversed_list + str(item) + '.' - LOGGER.debug("returning: %s", str(reversed_list) + lineno()) - return reversed_list - - LOGGER.info('Not a valid ip') - exit() - except: - LOGGER.info("unexpected error. %s\n", str(sys.exc_info()[0]) + lineno()) - -def get_reversed_domain_prefix(subnet_mask, private_ip): - """ - Uses the mask to get the zone prefix for the reverse lookup zone - :param subnet_mask: - :param private_ip: - :return: - """ - try: - LOGGER.debug("### Subnet mask: %s", str(subnet_mask) + lineno()) - LOGGER.debug("### Private ip: %s", str(private_ip) + lineno()) - - third_octet = re.search(r"\d{1,3}.\d{1,3}.\d{1,3}.", private_ip) - return third_octet.group(0) - 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): - """ - JSON serializer for objects not serializable by default json code - :param obj: - :return: - """ - try: - if isinstance(obj, datetime.datetime): - serial = obj.isoformat() - return serial - except: - LOGGER.info("unexpected error. %s\n", str(sys.exc_info()[0]) + lineno()) - -def remove_empty_from_dict(dictionary): - """ - Removes empty keys from dictionary - :param d: - :return: - """ - - try: - if isinstance(dictionary, dict): - return dict((k, remove_empty_from_dict(v)) for k, v in dictionary.items() \ - if v and remove_empty_from_dict(v)) - if isinstance(dictionary, list): - return [remove_empty_from_dict(v) for v in dictionary - if v and remove_empty_from_dict(v)] - - return 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): - """ - Whether dns hostnames is enabled - :param client: - :param vpc_id: - :return: - """ - try: - response = client.describe_vpc_attribute( - Attribute='enableDnsHostnames', - VpcId=vpc_id - ) - - LOGGER.debug("%s", str(response) + lineno()) - return response['EnableDnsHostnames']['Value'] - except: - LOGGER.info("unexpected error. %s\n", str(sys.exc_info()[0]) + lineno()) - -def is_dns_support_enabled(client, vpc_id): - """ - Whether dns support is enabled - :param client: - :param vpc_id: - :return: - """ - try: - response = client.describe_vpc_attribute( - Attribute='enableDnsSupport', - VpcId=vpc_id - ) - - LOGGER.debug('response2: %s', str(response) + lineno()) - return response['EnableDnsSupport']['Value'] - except: - LOGGER.info("unexpected error. %s\n", str(sys.exc_info()[0]) + lineno()) - -def get_hosted_zone_properties(client, zone_id): - """ - Get hosted zone properties - :param client: - :param zone_id: - :return: - """ - try: - LOGGER.debug('getting hosted zone properties: zone_id: %s', str(zone_id) + lineno()) - hosted_zone_properties = client.get_hosted_zone(Id=zone_id) - LOGGER.debug('hosted_zone_properties: %s', str(hosted_zone_properties) + lineno()) - if 'ResponseMetadata' in hosted_zone_properties: - hosted_zone_properties.pop('ResponseMetadata') - return hosted_zone_properties - except: - LOGGER.info("unexpected error. %s\n", str(sys.exc_info()[0]) + lineno()) - -def get_subnet_cidr_block(client, subnet_id): - """ - Get subnect cidr block - :param client: - :param subnet_id: - :return: - """ - try: - response = client.describe_subnets( - SubnetIds=[ - 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: +# 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 +""" +import json +import sys +import datetime +import random +import logging +import re +import uuid +import time +import inspect +import boto3 +import os +from botocore.exceptions import ClientError + +# Setting Global Variables +LOGGER = logging.getLogger() +ACCOUNT = None +REGION = None + +# 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['SleepTime']) +DDBNAME = os.environ['DynamoDBName'] +TAGKEY_CNAME = os.environ['TagKeyCname'] +TAGKEY_ZONE = os.environ['TagKeyZone'] +TAGKEY_HOSTNAME = os.environ['TagKeyHostName'] + +print('Loading function ' + datetime.datetime.now().time().isoformat()) + +def lineno(): # pragma: no cover + """ + Returns the current line number in our script + :return: + """ + 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 + :return: + """ + try: + return boto3.client('route53') + except ClientError as err: + print("Unexpected error: %s" % err) + +def get_ec2_client(): + """ + Get ec2 client + :return: + """ + try: + return boto3.client('ec2') + except ClientError as err: + print("Unexpected error: %s" % err) + +def get_dynamodb_client(): + """ + Get dynamodb client + :return: + """ + try: + return boto3.client('dynamodb') + except ClientError as err: + print("Unexpected error: %s" % err) + +def lambda_handler( + event, + context, + dynamodb_client=get_dynamodb_client(), + compute=get_ec2_client(), + route53=get_route53_client() +): + """ + Check to see whether a DynamoDB table already exists. If not, create it. + This table is used to keep a record of instances that have been created + along with their attributes. This is necessary because when you terminate an instance + its attributes are no longer available, so they have to be fetched from the table. + :param event: + :param context: + :param dynamodb_client: + :param compute: + :param route53: + :param sns_client: + :return: + """ + 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 + tables = list_tables(dynamodb_client) + + LOGGER.info("tables: %s", str(tables)) + if DDBNAME in tables['TableNames']: + LOGGER.info('DynamoDB table already exists') + 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 + state = event['detail']['state'] + LOGGER.debug("instance state: %s", str(state) + lineno()) + + # 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 + if state == 'running': + LOGGER.debug("sleeping for 60 seconds %s", lineno()) + + if "pytest" in sys.modules: + # called from within a test run + time.sleep(1) + else: + # called "normally" + time.sleep(SLEEPTIME) + + # Get instance information + instance = get_instances(compute, instance_id) + # Remove response metadata from the response + if 'ResponseMetadata' in instance: + instance.pop('ResponseMetadata') + # Remove null values from the response. You cannot save a dict/JSON + # document in DynamoDB if it contains null values + LOGGER.debug("instance: %s", str(instance) + lineno()) + instance = remove_empty_from_dict(instance) + instance_dump = json.dumps(instance, default=json_serial) + instance_attributes = json.loads(instance_dump) + LOGGER.debug("instance_attributes: %s", str(instance_attributes) + lineno()) + LOGGER.debug("trying to put instance information in " + "dynamo table %s", str(instance_attributes) + lineno()) + put_item_in_dynamodb_table(dynamodb_client, DDBNAME, instance_id, instance_attributes) + LOGGER.debug("done putting item in dynamo table %s", lineno()) + else: + # Fetch item from DynamoDB + LOGGER.debug("Fetching instance information from dynamodb %s", lineno()) + instance = get_item_from_dynamodb_table(dynamodb_client, DDBNAME, instance_id) + LOGGER.debug("instance: %s", str(instance) + lineno()) + + # Get the instance tags and reorder them because we want a zone created before CNAME + try: + tags = instance['Reservations'][0]['Instances'][0]['Tags'] + except: + tags = [] + + LOGGER.debug("tags are: %s", str(tags) + lineno()) + + # tag_type = determine_tag_type(tags) + tag_type = determine_tag_type(tags)[0] # changed to return a list, so read the 1st value + 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()) + if instance and 'Reservations' in instance: + LOGGER.debug("reservations: %s", str(instance['Reservations']) + lineno()) + LOGGER.debug("reservations: %s", str(instance['Reservations'][0]) + lineno()) + LOGGER.debug("reservations: %s", str(instance['Reservations'][0]['Instances']) + lineno()) + LOGGER.debug("reservations:" + " %s", str(instance['Reservations'][0]['Instances'][0]) + lineno()) + + private_ip = instance['Reservations'][0]['Instances'][0]['PrivateIpAddress'] + private_dns_name = instance['Reservations'][0]['Instances'][0]['PrivateDnsName'] + private_host_name = private_dns_name.split('.')[0] + + LOGGER.debug("private ip: %s", str(private_ip) + lineno()) + 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()) + cidr_block = get_subnet_cidr_block(compute, subnet_id) + LOGGER.debug("cidr_block: %s", str(cidr_block) + lineno()) + subnet_mask = int(cidr_block.split('/')[-1]) + LOGGER.debug("subnet_mask: %s", str(subnet_mask) + lineno()) + reversed_ip_address = reverse_list(private_ip) + + reversed_domain_prefix = get_reversed_domain_prefix(subnet_mask, private_ip) + reversed_domain_prefix = reverse_list(reversed_domain_prefix) + LOGGER.debug("reversed_domain_prefix is: %s", str(reversed_domain_prefix) + lineno()) + # Set the reverse lookup zone + reversed_lookup_zone = reversed_domain_prefix + 'in-addr.arpa.' + LOGGER.info("The reverse lookup zone for this instance is: %s", str(reversed_lookup_zone)) + + # Get VPC id + vpc_id = instance['Reservations'][0]['Instances'][0]['VpcId'] + + # Are DNS Hostnames and DNS Support enabled? + if is_dns_hostnames_enabled(compute, vpc_id): + LOGGER.debug("DNS hostnames enabled for %s", str(vpc_id) + lineno()) + else: + LOGGER.debug("DNS hostnames disabled for %s. You have to enable DNS hostnames to use Route 53 private hosted zones. %s", vpc_id, lineno()) + if is_dns_support_enabled(compute, vpc_id): + LOGGER.debug("DNS support enabled for %s", str(vpc_id) + lineno()) + else: + LOGGER.debug("DNS support disabled for %s. You have to enabled DNS support to use Route 53 private hosted zones. %s", str(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()) + private_hosted_zones = get_private_hosted_zones(hosted_zones) + LOGGER.debug("private_hosted_zones: %s", str(list(private_hosted_zones)) + lineno()) + private_hosted_zone_collection = get_private_hosted_zone_collection(private_hosted_zones) + 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. + LOGGER.info("reversed_lookup_zone: %s", str(reversed_lookup_zone) + lineno()) + reverse_zone = None + for record in hosted_zones['HostedZones']: + LOGGER.debug("record name: %s", str(record['Name']) + lineno()) + if record['Name'] == reversed_lookup_zone: + reverse_zone = record['Name'] + break + if reverse_zone: + LOGGER.debug("Reverse lookup zone found: %s", str(reversed_lookup_zone) + lineno()) + reverse_lookup_zone_id = get_zone_id(route53, reversed_lookup_zone) + LOGGER.debug("reverse_lookup_zone_id: %s", str(reverse_lookup_zone_id) + lineno()) + + reverse_hosted_zone_properties = get_hosted_zone_properties(route53, reverse_lookup_zone_id) + LOGGER.debug("reverse_hosted_zone_properties:" + " %s", str(reverse_hosted_zone_properties) + lineno()) + + if vpc_id in map(lambda x: x['VPCId'], reverse_hosted_zone_properties['VPCs']): + LOGGER.info("Reverse lookup zone %s is associated with VPC %s %s", reverse_lookup_zone_id, vpc_id, lineno()) + reverse_zone_associated = True + else: + LOGGER.info("Reverse lookup zone %s is NOT associated with VPC %s %s", 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() + # ) + + 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. + 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()) + 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', + 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(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', + 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(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')) + 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()) + + if TAGKEY_CNAME.upper() in tag.get('Key', {}).lstrip().upper(): + + # 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() + + LOGGER.debug("icname: %s", str(icname) + lineno()) + # Gets the prefix for the cname + cname_host_name = icname.split('.')[0] + LOGGER.debug("cname_host_name: %s", str(cname_host_name) + lineno()) + + # Gets suffix + cname_domain_suffix = icname[icname.find('.') + 1:] + LOGGER.debug("cname_domain_suffix: %s", str(cname_domain_suffix) + lineno()) + + # Try and find the hosted zone with the cname suffix + 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()) + + 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. 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: + + 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()) + + # create A records and PTR records + 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', + 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(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: + + 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', + 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(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()) + else: + LOGGER.debug("No matching zone for %s", str(configuration) + lineno()) + + # Clean up DynamoDB after deleting records + if state != 'running': + delete_item_from_dynamodb_table(dynamodb_client, DDBNAME, instance_id) + + caller_response.insert(0, 'Successfully removed recordsets') + return caller_response + + caller_response.insert(0, 'Successfully created recordsets') + + 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 + :param tags: + :return: + """ + + try: + for tag in tags: + LOGGER.debug("tag: %s", str(tag)) + + if TAGKEY_CNAME.upper() in tag.get('Key', {}).lstrip().upper(): + cname = tag.get('Value').lstrip().lower() + + return cname + return None + except: + LOGGER.info("unexpected error. %s\n", str(sys.exc_info()[0])) + +def get_instances(client, instance_id): + """ + Get ec2 instance information + :return: + """ + try: + return client.describe_instances(InstanceIds=[instance_id]) + except ClientError as err: + LOGGER.info("unexpected error. %s\n", str(err) + lineno()) + +def list_hosted_zones(client): + """ + Get route53 hosted zones + :param client: + :return: + """ + try: + return client.list_hosted_zones() + except ClientError as err: + LOGGER.info("unexpected error. %s\n", str(err) + lineno()) + +def list_tables(client): + """ + List the dynamodb tables + :param client: + :return: + """ + try: + return client.list_tables() + except ClientError as err: + LOGGER.info("unexpected error. %s\n", str(err) + lineno()) + +def delete_item_from_dynamodb_table(client, table, instance_id): + """ + Delete the item from dynamodb table + :param client: + :param table: + :param instance_id: + :return: + """ + try: + return client.delete_item( + TableName=table, + Key={ + 'InstanceId': {'S': instance_id} + }) + except ClientError as err: + LOGGER.info("unexpected error. %s\n", str(err) + lineno()) + +def put_item_in_dynamodb_table(client, table, instance_id, instance_attributes): + """ + Put item in dynamodb table + :param client: + :param table: + :param instance_id: + :param instance_attributes: + :return: + """ + try: + LOGGER.debug("attributes: %s", str(instance_attributes) + lineno()) + LOGGER.debug("putting attributes: %s", str(instance_attributes) + lineno()) + + return client.put_item( + TableName=str(table), + Item={ + 'InstanceId': {'S': instance_id}, + 'InstanceAttributes': {'S': str(instance_attributes)} + } + ) + except ClientError as err: + LOGGER.info("unexpected error. %s\n", str(err) + lineno()) + +def get_item_from_dynamodb_table(client, table, instance_id): + """ + Get item from dynamodb table + :param client: + :param table: + :param instance_id: + :return: + """ + try: + # Fetch item from DynamoDB + item = client.get_item( + TableName=table, + Key={ + 'InstanceId': { + 'S': instance_id + } + }, + AttributesToGet=[ + 'InstanceAttributes' + ] + ) + + if 'Item' in item: + LOGGER.debug("returned item:" + " %s", str(item['Item']['InstanceAttributes']['S']) + lineno()) + item = item['Item']['InstanceAttributes']['S'].replace("'", '"') + item = item.replace(" True,", ' "True",') + item = item.replace(" False,", ' "False",') + LOGGER.debug("item: %s", str(item) + lineno()) + return json.loads(item) + return None + except ClientError as err: + LOGGER.info("unexpected error. %s\n", str(err) + lineno()) + +def get_private_hosted_zone_collection(private_hosted_zones): + """ + Get private hosted zone collection + :param private_hosted_zones: + :return: + """ + try: + private_hosted_zone_collection = [] + + for item in private_hosted_zones: + LOGGER.debug("item: %s", str(item) + lineno()) + private_hosted_zone_collection.append(item['Name']) + + return private_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_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): + """ + Get private hosted zones + :param hosted_zones: + :return: + """ + try: + private_hosted_zones = [] + + for item in hosted_zones['HostedZones']: + LOGGER.debug("item: %s", str(item) + lineno()) + + if item['Config']['PrivateZone']: + private_hosted_zones.append(item) + + return private_hosted_zones + except: + LOGGER.info("unexpected error. %s\n", str(sys.exc_info()[0]) + lineno()) + +def get_dhcp_option_set_id_for_vpc(client, vpc_id): + """ + Get the dhcp option set from vpc + :param client: + :param vpc_id: + :return: + """ + try: + option_sets = {} + + results = client.describe_vpcs() + + for item in results['Vpcs']: + + if 'DhcpOptionsId' in item: + option_sets[str(item['VpcId'])] = item['DhcpOptionsId'] + else: + option_sets[str(item['VpcId'])] = None + + return option_sets[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): + """ + Get the dynamodb table + :param client: + :param table_name: + :return: + """ + try: + return client.describe_table( + TableName=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 + :param client: + :param zone_id: + :param host_name: + :param hosted_zone_name: + :param value: + :return: + """ + try: + response = client.change_resource_record_sets( + HostedZoneId=zone_id, + ChangeBatch={ + "Comment": "Updated by Lambda DDNS", + "Changes": [ + { + "Action": "UPSERT", + "ResourceRecordSet": { + "Name": host_name + hosted_zone_name, + "Type": record_type, + "TTL": 60, + "ResourceRecords": [ + { + "Value": value + }, + ] + } + }, + ] + } + ) + + LOGGER.debug("response: %s", str(response) + lineno()) + return response + except ClientError as err: + LOGGER.debug("Error creating resource record: %s", str(err) + lineno()) + error_message = str(err) + + if "conflicts with other records" in error_message: + LOGGER.debug("Can not create dns record because of duplicates: %s", str(err) + lineno()) + return 'Duplicate resource record' + elif "conflicting RRSet" in error_message: + LOGGER.debug("Can not create dns record because of duplicates: %s", str(err) + lineno()) + return 'Conflicting resource record' + else: + LOGGER.info("unexpected error. %s\n", str(err) + lineno()) + return 'Unexpected error: ' + str(err) + +def create_resource_record(client, zone_id, host_name, hosted_zone_name, record_type, value): + """ + This function creates resource records in the hosted zone passed by the calling function. + :param client: + :param zone_id: + :param host_name: + :param hosted_zone_name: + :param record_type: + :param value: + :return: + """ + LOGGER.debug("Creating resource record: zone_id: %s host_name:" + " %s hosted_zone_name: %s record_type: %s value: %s %s", zone_id, + host_name, hosted_zone_name, record_type, value, lineno()) + try: + if host_name[-1] != '.': + host_name = host_name + '.' + + LOGGER.debug( + "Updating %s in zone %s%s to %s %s", record_type, host_name, + hosted_zone_name, value, lineno()) + + # To prevent rate throttling + time.sleep(1) + + response = change_resource_recordset( + client, + zone_id, + host_name, + hosted_zone_name, + record_type, + value + ) + + LOGGER.debug("response: %s", str(response) + lineno()) + return response + except ClientError as err: + LOGGER.debug("Error creating resource record: %s", str(err) + lineno()) + if 'is not permitted as it conflicts with other records ' \ + 'with the same DNS name in zone' in str(err): + LOGGER.debug("Can not create dns record because " + "of duplicates: %s", str(err) + lineno()) + +def delete_resource_record(client, zone_id, host_name, hosted_zone_name, record_type, value): + """ + This function deletes resource records from the hosted zone passed by the calling function. + :param client: + :param zone_id: + :param host_name: + :param hosted_zone_name: + :param record_type: + :param value: + :return: + """ + try: + LOGGER.debug("Deleting %s record %s in zone %s" + " %s", record_type, host_name, hosted_zone_name, lineno()) + if host_name[-1] != '.': + host_name = host_name + '.' + response = client.change_resource_record_sets( + HostedZoneId=zone_id, + ChangeBatch={ + "Comment": "Updated by Lambda DDNS", + "Changes": [ + { + "Action": "DELETE", + "ResourceRecordSet": { + "Name": host_name + hosted_zone_name, + "Type": record_type, + "TTL": 60, + "ResourceRecords": [ + { + "Value": value + }, + ] + } + } + ] + } + ) + + LOGGER.debug("delete record response: %s", str(response) + lineno()) + return response + + except ClientError as err: + if 'Not Found' in str(err): + LOGGER.debug("Record not found error: %s", str(err)+lineno()) + return + + if 'InvalidChangeBatch' in str(err) and 'it was not found' in str(err): + LOGGER.debug("Record not found error: %s", str(err)+lineno()) + return + + LOGGER.info("unexpected error. %s\n", str(err) + lineno()) + +def get_zone_id(client, zone_name, private_zone=True): + """ + This function returns the zone id for the zone name that's passed into the function. + :param client: + :param zone_name: + :return: + """ + try: + if zone_name[-1] != '.': + zone_name = zone_name + '.' + hosted_zones = list_hosted_zones(client) + + LOGGER.debug("zone name: %s", str(zone_name) + lineno()) + LOGGER.debug("hosted_zones: %s", str(hosted_zones) + lineno()) + zones = [] + for record in hosted_zones['HostedZones']: + LOGGER.debug("record: %s", str(record) + lineno()) + if record['Config']['PrivateZone'] == private_zone: + if record['Name'] == zone_name: + zones.append(record) + LOGGER.debug("zones: %s", str(zones) + lineno()) + + try: + zone_id_long = zones[0]['Id'] + LOGGER.debug("zone id: %s", str(zone_id_long) + lineno()) + zone_id = str.split(str(zone_id_long), '/')[2] + return zone_id + except: + return None + except: + LOGGER.info("unexpected error. %s\n", str(sys.exc_info()[0]) + lineno()) + +def is_valid_hostname(hostname): + """ + This function checks to see whether the hostname entered + into the zone and cname tags is a valid hostname. + :param hostname: + :return: + """ + try: + LOGGER.debug("determining if hostname is valid: %s", str(hostname) + lineno()) + if hostname is None or len(hostname) > 255: + return False + if hostname[-1] == ".": + hostname = hostname[:-1] + allowed = re.compile(r"(?!-)[A-Z\d-]{1,63}(? 0: + my_list.append(int(item)) + + LOGGER.debug("list1: %s", str(my_list) + lineno()) + LOGGER.debug("type: %s", str(type(my_list)) + lineno()) + + my_list.reverse() + reversed_list = '' + for item in my_list: + reversed_list = reversed_list + str(item) + '.' + LOGGER.debug("returning: %s", str(reversed_list) + lineno()) + return reversed_list + + LOGGER.info('Not a valid ip') + exit() + except: + LOGGER.info("unexpected error. %s\n", str(sys.exc_info()[0]) + lineno()) + +def get_reversed_domain_prefix(subnet_mask, private_ip): + """ + Uses the mask to get the zone prefix for the reverse lookup zone + :param subnet_mask: + :param private_ip: + :return: + """ + try: + LOGGER.debug("### Subnet mask: %s", str(subnet_mask) + lineno()) + LOGGER.debug("### Private ip: %s", str(private_ip) + lineno()) + + third_octet = re.search(r"\d{1,3}.\d{1,3}.\d{1,3}.", private_ip) + return third_octet.group(0) + 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): + """ + JSON serializer for objects not serializable by default json code + :param obj: + :return: + """ + try: + if isinstance(obj, datetime.datetime): + serial = obj.isoformat() + return serial + except: + LOGGER.info("unexpected error. %s\n", str(sys.exc_info()[0]) + lineno()) + +def remove_empty_from_dict(dictionary): + """ + Removes empty keys from dictionary + :param d: + :return: + """ + + try: + if isinstance(dictionary, dict): + return dict((k, remove_empty_from_dict(v)) for k, v in dictionary.items() \ + if v and remove_empty_from_dict(v)) + if isinstance(dictionary, list): + return [remove_empty_from_dict(v) for v in dictionary + if v and remove_empty_from_dict(v)] + + return 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): + """ + Whether dns hostnames is enabled + :param client: + :param vpc_id: + :return: + """ + try: + response = client.describe_vpc_attribute( + Attribute='enableDnsHostnames', + VpcId=vpc_id + ) + + LOGGER.debug("%s", str(response) + lineno()) + return response['EnableDnsHostnames']['Value'] + except: + LOGGER.info("unexpected error. %s\n", str(sys.exc_info()[0]) + lineno()) + +def is_dns_support_enabled(client, vpc_id): + """ + Whether dns support is enabled + :param client: + :param vpc_id: + :return: + """ + try: + response = client.describe_vpc_attribute( + Attribute='enableDnsSupport', + VpcId=vpc_id + ) + + LOGGER.debug('response2: %s', str(response) + lineno()) + return response['EnableDnsSupport']['Value'] + except: + LOGGER.info("unexpected error. %s\n", str(sys.exc_info()[0]) + lineno()) + +def get_hosted_zone_properties(client, zone_id): + """ + Get hosted zone properties + :param client: + :param zone_id: + :return: + """ + try: + LOGGER.debug('getting hosted zone properties: zone_id: %s', str(zone_id) + lineno()) + hosted_zone_properties = client.get_hosted_zone(Id=zone_id) + LOGGER.debug('hosted_zone_properties: %s', str(hosted_zone_properties) + lineno()) + if 'ResponseMetadata' in hosted_zone_properties: + hosted_zone_properties.pop('ResponseMetadata') + return hosted_zone_properties + except: + LOGGER.info("unexpected error. %s\n", str(sys.exc_info()[0]) + lineno()) + +def get_subnet_cidr_block(client, subnet_id): + """ + Get subnect cidr block + :param client: + :param subnet_id: + :return: + """ + try: + response = client.describe_subnets( + SubnetIds=[ + 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()) \ No newline at end of file