From 26dc1426b77feba8567ed629de8bcf7846b58bae Mon Sep 17 00:00:00 2001 From: badra001 Date: Fri, 21 Jan 2022 11:32:47 -0500 Subject: [PATCH 01/33] initial --- .gitmodules | 3 + .../availabilty_zones.tf | 0 .../variables.common.availability_zones.tf | 0 CHANGELOG.md | 2 +- code/ddns-lambda.py | 1661 +++++++++++++++++ dynamodb.tf | 32 + examples/test/test.tf | 4 + locals.tf.initial => locals.tf | 3 + variables.common.tf | 6 + variables.tf | 14 + version.tf | 2 +- 11 files changed, 1725 insertions(+), 2 deletions(-) create mode 100644 .gitmodules rename availabilty_zones.tf => .off/availabilty_zones.tf (100%) rename variables.common.availability_zones.tf => .off/variables.common.availability_zones.tf (100%) create mode 100644 code/ddns-lambda.py create mode 100644 dynamodb.tf create mode 100644 examples/test/test.tf rename locals.tf.initial => locals.tf (78%) create mode 100644 variables.tf diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..68cc742 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "aws-lambda-ddns-function"] + path = aws-lambda-ddns-function + url = https://github.com/aws-samples/aws-lambda-ddns-function diff --git a/availabilty_zones.tf b/.off/availabilty_zones.tf similarity index 100% rename from availabilty_zones.tf rename to .off/availabilty_zones.tf diff --git a/variables.common.availability_zones.tf b/.off/variables.common.availability_zones.tf similarity index 100% rename from variables.common.availability_zones.tf rename to .off/variables.common.availability_zones.tf diff --git a/CHANGELOG.md b/CHANGELOG.md index 2ce3418..257bcda 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,5 @@ # Versions -* v1.0.0 -- {{ yyyy-mm-dd }} +* v1.0.0 -- 2022-01-21 - initial creation diff --git a/code/ddns-lambda.py b/code/ddns-lambda.py new file mode 100644 index 0000000..f0f9acf --- /dev/null +++ b/code/ddns-lambda.py @@ -0,0 +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: +# LOGGER.debug("Unexpected error: %s", str(err)+lineno()) \ No newline at end of file diff --git a/dynamodb.tf b/dynamodb.tf new file mode 100644 index 0000000..e6f2f0c --- /dev/null +++ b/dynamodb.tf @@ -0,0 +1,32 @@ +locals { + dynamodb_table = var.dynamodb_table != null ? var.dynamodb_table : local.name +} + +resource "aws_dynamodb_table" "table" { + name = local.dynamodb_table + hash_key = "InstanceId" + billing_mode = "PROVISIONED" + read_capacity = 4 + write_capacity = 4 + + attribute { + name = "InstanceId" + type = "S" + } + + server_side_encryption { + enabled = true + } + + tags = merge( + local.base_tags, + var.tags, + lookup(var.component_tags, "ddb", {}), + map("Name", local.dynamodb_table), + ) + + lifecycle { + ignore_changes = [tags["boc:tf_module_version"]] + } + +} diff --git a/examples/test/test.tf b/examples/test/test.tf new file mode 100644 index 0000000..7e809d5 --- /dev/null +++ b/examples/test/test.tf @@ -0,0 +1,4 @@ +module "dynamic-route53" { + source = "git@github.e.it.census.gov:terraform-modules/aws-dynamic-route53.git?ref=initial" + +} diff --git a/locals.tf.initial b/locals.tf similarity index 78% rename from locals.tf.initial rename to locals.tf index 2bd4d7f..354d815 100644 --- a/locals.tf.initial +++ b/locals.tf @@ -1,9 +1,12 @@ locals { account_id = var.account_id != "" ? var.account_id : data.aws_caller_identity.current.account_id account_environment = data.aws_arn.current.partition == "aws-us-gov" ? "gov" : "ew" + region = data.aws_region.current.name base_tags = { "boc:tf_module_version" = local._module_version "boc:created_by" = "terraform" } + + name = format("%v-%v",var.name,local.region) } diff --git a/variables.common.tf b/variables.common.tf index c77ef47..afbd2a7 100644 --- a/variables.common.tf +++ b/variables.common.tf @@ -24,3 +24,9 @@ variable "tags" { type = map(string) default = {} } + +variable "component_tags" { + description = "Additional tags for Components (s3, kms, ddb)" + type = map(map(string)) + default = { "s3" = {}, "kms" = {}, "ddb" = {} } +} diff --git a/variables.tf b/variables.tf new file mode 100644 index 0000000..ed84625 --- /dev/null +++ b/variables.tf @@ -0,0 +1,14 @@ +variable "name" { + description = "Name to use within all the created resources (default: inf-dynamic-route53)" + type = string + default = "inf-dynamic-route53" +} + + +variable "dynamodb_table" { + description = "Different DynamoDB table to override default of var.name) + type = string + default = null +} + + diff --git a/version.tf b/version.tf index a0cd862..fa2705b 100644 --- a/version.tf +++ b/version.tf @@ -1,3 +1,3 @@ locals { - _module_version = "0.0.0" + _module_version = "1.0.0" } From 9ea7fbde9a261a74ebfaefad74c86b84ea90c01e Mon Sep 17 00:00:00 2001 From: badra001 Date: Fri, 21 Jan 2022 11:34:51 -0500 Subject: [PATCH 02/33] update .pre-commit --- .pre-commit-config.yaml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 93cda0b..4e10255 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,11 +1,12 @@ repos: - repo: https://github.com/antonbabenko/pre-commit-terraform - rev: v1.48.0 + rev: v1.62.3 hooks: # - id: terraform_validate - id: terraform_fmt - - id: terraform_docs_replace - args: ['table'] + - id: terraform_docs + args: + - --args=--config=.terraform-docs.yml exclude: common/*.tf exclude: version.tf exclude: examples/ @@ -13,7 +14,7 @@ repos: args: [ "--args=--config=__GIT_WORKING_DIR__/.tflint.hcl"] exclude: examples/ - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.4.0 + rev: v4.1.0 hooks: - id: check-symlinks - id: detect-aws-credentials From e745e30f37f612460c84fec5580493e803e8bdac Mon Sep 17 00:00:00 2001 From: badra001 Date: Fri, 21 Jan 2022 11:36:05 -0500 Subject: [PATCH 03/33] fix --- .terraform-docs.yml | 45 +++++++++++++++++ README.md | 116 ++++++++++++-------------------------------- locals.tf | 4 +- variables.tf | 10 ++-- versions.tf | 2 +- 5 files changed, 84 insertions(+), 93 deletions(-) create mode 100644 .terraform-docs.yml diff --git a/.terraform-docs.yml b/.terraform-docs.yml new file mode 100644 index 0000000..418f24a --- /dev/null +++ b/.terraform-docs.yml @@ -0,0 +1,45 @@ +formatter: markdown table + +header-from: main.tf +footer-from: "" + +sections: +## hide: [] + show: + - data-sources + - header + - footer + - inputs + - modules + - outputs + - providers + - requirements + - resources + +output: + file: README.md + mode: replace +# mode: inject +# template: |- +# +# {{ .Content }} +# + +## output-values: +## enabled: false +## from: "" +## +## sort: +## enabled: true +## by: name +## +## settings: +## anchor: true +## color: true +## default: true +## description: false +## escape: true +## indent: 2 +## required: true +## sensitive: true +## type: true diff --git a/README.md b/README.md index 659a771..eec47f0 100644 --- a/README.md +++ b/README.md @@ -1,96 +1,42 @@ -# aws-dynamic-route53 + +## Requirements -## About +| Name | Version | +|------|---------| +| [aws](#requirement\_aws) | >= 3.66.0 | -This module will construct all the resources to allow for automated C2 DNS registration in Route53. This is largely sourced from the -AWS blog on [DNS in a Multiaccount Environment with Route53](https://aws.amazon.com/blogs/security/simplify-dns-management-in-a-multiaccount-environment-with-route-53-resolver/) . We have added to it to also do PTR registration, as well as -making it IPv6 ready. +## Providers -The [code](https://github.com/aws-samples/aws-lambda-ddns-function) from that blog is linked in as a submodule under [aws-lambda-ddns-function](aws-lambda-ddns-function/). +| Name | Version | +|------|---------| +| [aws](#provider\_aws) | >= 3.66.0 | -This code is intended to be deployed per region, and will handle all of the DNS registration for EC2 -instances deployed, assumign specific tags exist. +## Modules -It will create: +No modules. -- DynamoDB Table (inf-dynamic-route53-{region}) -- IAM Roles -- Lambda -- CloudWatch Events -- CloudWatch Log +## Resources -## Operation +| Name | Type | +|------|------| +| [aws_dynamodb_table.table](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/dynamodb_table) | resource | +| [aws_arn.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/arn) | data source | +| [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | +| [aws_region.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source | -See the the [blog](#blog) for full details on how it works. The short version is: +## Inputs -- Cloudwatch event on instance (starting, started, terminated) -- Run lambda -- On startup - - Get instance details (id, region, ipv4, ipv6) - - Determine zone from tag(s) - - Find zone - - Add records if found - - Log action - - Record in DDB name and details -- On terminate - - Get instance detail (id) - - search DDB table for id - - Remove records, if in table - - Log action +| Name | Description | Type | Default | Required | +|------|-------------|------|---------|:--------:| +| [account\_alias](#input\_account\_alias) | AWS Account Alias | `string` | `""` | no | +| [account\_id](#input\_account\_id) | AWS Account ID (default will pull from current user) | `string` | `""` | no | +| [component\_tags](#input\_component\_tags) | Additional tags for Components (s3, kms, ddb) | `map(map(string))` |
{
"ddb": {},
"kms": {},
"s3": {}
}
| no | +| [dynamodb\_table](#input\_dynamodb\_table) | Different DynamoDB table to override default of var.name) | `string` | `null` | no | +| [name](#input\_name) | Name to use within all the created resources (default: inf-dynamic-route53) | `string` | `"inf-dynamic-route53"` | no | +| [override\_prefixes](#input\_override\_prefixes) | Override built-in prefixes by component. This should be used primarily for common infrastructure things | `map(string)` | `{}` | no | +| [tags](#input\_tags) | AWS Tags to apply to appropriate resources | `map(string)` | `{}` | no | +## Outputs -## Tags - -A number of tags will be used to affect behavior of the DNS entries. - -### Tag: Name - -The `Name` tag is the primary tag that will be used to determine the DNS name to create. It is expected to be a unique FQDN. If no Name -tag is provided, the hostname portion of the name will be constructed from the IP address: - -* IPv4 - * ip address: A.B.C.D - * hostname: ip-A-B-C-D -* IPv6 (TBD) - -The domain portion of the `Name` tag must exist within Route53 in order for any records to be created. - -### Tag: boc:dns:zone - -The `boc:dns:zone` tag will be used in case we need to force a specific domain name on a host, either because it cannot obtain -the proper zone (domain) from the `Name` tag, of that a custom per-instance `Name` tag cannot be created. This latter condition -occurs for systems which work from a launch template, such as EMR or EKS. - -### Tag: boc:dns:alias - -The `boc:dns:alias` tag is used to create an alternate DNS name (CNAME), pointed to the primary name. It is an FQDN, and the same conditions -apply as with [Name](#tag--name). - -# Links - -## github aws-lambda-ddns-function - * https://github.com/aws-samples/aws-lambda-ddns-function -## Blog - * https://aws.amazon.com/blogs/security/simplify-dns-management-in-a-multiaccount-environment-with-route-53-resolver/ - - -# Repository Setup Details - -* One time - -```script -git submodule add https://github.com/aws-samples/aws-lambda-ddns-function aws-lambda-ddns-function -git commit -m'add submodule' aws-lambda-ddns-function -``` - -* After first clone - -```script -git submodule update --init -``` - -* Pull new stuff from submoduule - -```script -git submodule foreach git pull origin master -``` +No outputs. + \ No newline at end of file diff --git a/locals.tf b/locals.tf index 354d815..3f27853 100644 --- a/locals.tf +++ b/locals.tf @@ -1,12 +1,12 @@ locals { account_id = var.account_id != "" ? var.account_id : data.aws_caller_identity.current.account_id account_environment = data.aws_arn.current.partition == "aws-us-gov" ? "gov" : "ew" - region = data.aws_region.current.name + region = data.aws_region.current.name base_tags = { "boc:tf_module_version" = local._module_version "boc:created_by" = "terraform" } - name = format("%v-%v",var.name,local.region) + name = format("%v-%v", var.name, local.region) } diff --git a/variables.tf b/variables.tf index ed84625..86415fe 100644 --- a/variables.tf +++ b/variables.tf @@ -1,14 +1,14 @@ variable "name" { description = "Name to use within all the created resources (default: inf-dynamic-route53)" - type = string - default = "inf-dynamic-route53" + type = string + default = "inf-dynamic-route53" } variable "dynamodb_table" { - description = "Different DynamoDB table to override default of var.name) - type = string - default = null + description = "Different DynamoDB table to override default of var.name)" + type = string + default = null } diff --git a/versions.tf b/versions.tf index 4ba10ce..34eb3b9 100644 --- a/versions.tf +++ b/versions.tf @@ -5,5 +5,5 @@ terraform { version = ">= 3.66.0" } } -# required_version = ">= 0.13" + # required_version = ">= 0.13" } From fd4d13868373aec8635823163f48eebfa1d12a6f Mon Sep 17 00:00:00 2001 From: badra001 Date: Fri, 21 Jan 2022 12:01:54 -0500 Subject: [PATCH 04/33] disable tf module pinned source --- .tflint.hcl | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.tflint.hcl b/.tflint.hcl index fcc2fa8..09d6863 100644 --- a/.tflint.hcl +++ b/.tflint.hcl @@ -19,3 +19,9 @@ rule "aws_instance_invalid_type" { plugin "aws" { enabled = true } + +# https://github.com/terraform-linters/tflint/blob/v0.33.1/docs/rules/terraform_module_pinned_source.md +rule "terraform_module_pinned_source" { + enabled = false +} + From c5f0f7d58f420b1054232d2351c331261af74050 Mon Sep 17 00:00:00 2001 From: badra001 Date: Fri, 21 Jan 2022 12:01:58 -0500 Subject: [PATCH 05/33] add role and policy --- README.md | 11 +++++-- dynamodb.tf | 6 ++-- role.tf | 75 +++++++++++++++++++++++++++++++++++++++++++++ variables.create.tf | 5 +++ variables.tf | 9 ++++-- version.tf | 2 +- 6 files changed, 100 insertions(+), 8 deletions(-) create mode 100644 role.tf create mode 100644 variables.create.tf diff --git a/README.md b/README.md index eec47f0..3cff5f0 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,9 @@ ## Modules -No modules. +| Name | Source | Version | +|------|--------|---------| +| [role](#module\_role) | git@github.e.it.census.gov:terraform-modules/aws-iam-role.git | n/a | ## Resources @@ -22,6 +24,9 @@ No modules. | [aws_dynamodb_table.table](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/dynamodb_table) | resource | | [aws_arn.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/arn) | data source | | [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | +| [aws_iam_policy.lambda_policies](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy) | data source | +| [aws_iam_policy_document.lambda_assume](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | +| [aws_iam_policy_document.lambda_policy](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy_document) | data source | | [aws_region.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/region) | data source | ## Inputs @@ -31,7 +36,9 @@ No modules. | [account\_alias](#input\_account\_alias) | AWS Account Alias | `string` | `""` | no | | [account\_id](#input\_account\_id) | AWS Account ID (default will pull from current user) | `string` | `""` | no | | [component\_tags](#input\_component\_tags) | Additional tags for Components (s3, kms, ddb) | `map(map(string))` |
{
"ddb": {},
"kms": {},
"s3": {}
}
| no | -| [dynamodb\_table](#input\_dynamodb\_table) | Different DynamoDB table to override default of var.name) | `string` | `null` | no | +| [create](#input\_create) | Flag to indicate whether to create the resources or not (default: true) | `bool` | `true` | no | +| [dynamodb\_table\_name](#input\_dynamodb\_table\_name) | Different DynamoDB table name to override default of var.name) | `string` | `null` | no | +| [lambda\_name](#input\_lambda\_name) | Different Lambda name to override default of var.name) | `string` | `null` | no | | [name](#input\_name) | Name to use within all the created resources (default: inf-dynamic-route53) | `string` | `"inf-dynamic-route53"` | no | | [override\_prefixes](#input\_override\_prefixes) | Override built-in prefixes by component. This should be used primarily for common infrastructure things | `map(string)` | `{}` | no | | [tags](#input\_tags) | AWS Tags to apply to appropriate resources | `map(string)` | `{}` | no | diff --git a/dynamodb.tf b/dynamodb.tf index e6f2f0c..0a83d84 100644 --- a/dynamodb.tf +++ b/dynamodb.tf @@ -1,9 +1,9 @@ locals { - dynamodb_table = var.dynamodb_table != null ? var.dynamodb_table : local.name + dynamodb_table_name = var.dynamodb_table_name != null ? var.dynamodb_table_name : local.name } resource "aws_dynamodb_table" "table" { - name = local.dynamodb_table + name = local.dynamodb_table_name hash_key = "InstanceId" billing_mode = "PROVISIONED" read_capacity = 4 @@ -22,7 +22,7 @@ resource "aws_dynamodb_table" "table" { local.base_tags, var.tags, lookup(var.component_tags, "ddb", {}), - map("Name", local.dynamodb_table), + map("Name", local.dynamodb_table_name), ) lifecycle { diff --git a/role.tf b/role.tf new file mode 100644 index 0000000..b2a4a8f --- /dev/null +++ b/role.tf @@ -0,0 +1,75 @@ +locals { + lambda_name = var.lambda_name != null ? var.lambda_name : local.name + lambda_policies = ["AWSLambdaBasicExecutionRole"] +} + +module "role" { + source = "git@github.e.it.census.gov:terraform-modules/aws-iam-role.git" + + role_description = "Lambda role for Dynamic Route53" + role_name = local.lambda_name + enable_ldap_creation = false + assume_policy_document = data.aws_iam_policy_document.lambda_assume.json + attached_policies = [for k, v in data.aws_iam_policy.lambda_policies : k.arn] + inline_policies = [{ name = var.name, policy = data.aws_iam_policy_document.lambda_policy.json }] +} + +data "aws_iam_policy" "lambda_policies" { + for_each = toset(local.lambda_policies) + name = each.key +} + +data "aws_iam_policy_document" "lambda_policy" { + statement { + sid = "AllowRoute53" + effect = "Allow" + actions = [ + "route53:ListHostedZones*", + "route53:ListResourceRecordSets", + "route53:GetHostedZone*", + "route53:ChangeResourceRecordSets", + ] + resources = ["*"] + } + statement { + sid = "EC2" + effect = "Allow" + actions = ["ec2:Describe*"] + resources = ["*"] + } + statement { + sid = "DynamoDBGlobal" + effect = "Allow" + actions = ["dynamodb:ListTables"] + resources = ["*"] + } + statement { + sid = "DynamoDBTable" + effect = "Allow" + actions = [ + "dynamodb:BatchGet*", + "dynamodb:DeleteItem", + "dynamodb:Describe*", + "dynamodb:Get*", + "dynamodb:List*", + "dynamodb:PutItem", + "dynamodb:Query", + "dynamodb:Scan", + "dynamodb:UpdateItem", + ] + resources = [aws_dynamodb_table.table.arn] + } +} + +data "aws_iam_policy_document" "lambda_assume" { + statement { + sid = "LambdaAssumeRole" + effect = "Allow" + actions = ["sts:AssumeRole"] + + principals { + type = "Service" + identifiers = ["lambda.amazonaws.com"] + } + } +} diff --git a/variables.create.tf b/variables.create.tf new file mode 100644 index 0000000..7613cac --- /dev/null +++ b/variables.create.tf @@ -0,0 +1,5 @@ +variable "create" { + description = "Flag to indicate whether to create the resources or not (default: true)" + type = bool + default = true +} diff --git a/variables.tf b/variables.tf index 86415fe..e1c9ca6 100644 --- a/variables.tf +++ b/variables.tf @@ -4,9 +4,14 @@ variable "name" { default = "inf-dynamic-route53" } +variable "dynamodb_table_name" { + description = "Different DynamoDB table name to override default of var.name)" + type = string + default = null +} -variable "dynamodb_table" { - description = "Different DynamoDB table to override default of var.name)" +variable "lambda_name" { + description = "Different Lambda name to override default of var.name)" type = string default = null } diff --git a/version.tf b/version.tf index fa2705b..0d48594 100644 --- a/version.tf +++ b/version.tf @@ -1,3 +1,3 @@ locals { - _module_version = "1.0.0" + _module_version = "0.0.2" } From e31beb8ee8cc88b33c10afbdd062d6cf0b161556 Mon Sep 17 00:00:00 2001 From: badra001 Date: Fri, 21 Jan 2022 12:22:56 -0500 Subject: [PATCH 06/33] remove role module --- README.md | 6 +++--- role.tf | 37 +++++++++++++++++++++++++++++-------- 2 files changed, 32 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 3cff5f0..dca4c1e 100644 --- a/README.md +++ b/README.md @@ -13,15 +13,15 @@ ## Modules -| Name | Source | Version | -|------|--------|---------| -| [role](#module\_role) | git@github.e.it.census.gov:terraform-modules/aws-iam-role.git | n/a | +No modules. ## Resources | Name | Type | |------|------| | [aws_dynamodb_table.table](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/dynamodb_table) | resource | +| [aws_iam_role.role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | +| [aws_iam_role_policy_attachment.role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | | [aws_arn.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/arn) | data source | | [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | | [aws_iam_policy.lambda_policies](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy) | data source | diff --git a/role.tf b/role.tf index b2a4a8f..1dfec49 100644 --- a/role.tf +++ b/role.tf @@ -3,17 +3,38 @@ locals { lambda_policies = ["AWSLambdaBasicExecutionRole"] } -module "role" { - source = "git@github.e.it.census.gov:terraform-modules/aws-iam-role.git" +resource "aws_iam_role" "role" { + count = var.create ? 1 : 0 + description = "Lambda role for Dynamic Route53" + name = format("%v%v", local.lambda_name) + force_detach_policies = local._defaults["force_detach_policies"] + max_session_duration = var.max_session_duration + assume_role_policy = data.aws_iam_policy_document.lambda_assume.json - role_description = "Lambda role for Dynamic Route53" - role_name = local.lambda_name - enable_ldap_creation = false - assume_policy_document = data.aws_iam_policy_document.lambda_assume.json - attached_policies = [for k, v in data.aws_iam_policy.lambda_policies : k.arn] - inline_policies = [{ name = var.name, policy = data.aws_iam_policy_document.lambda_policy.json }] + inline_policy = { + name = var.name + policy = data.aws_iam_policy_document.lambda_policy.json + } + + lifecycle { + ignore_changes = [tags["boc:tf_module_version"]] + } + + tags = merge( + local.base_tags, + var.tags, + lookup(var.component_tags, "role", {}), + tomap({ Name = local.lambda_name }) + ) } +resource "aws_iam_role_policy_attachment" "role" { + for_each = var.create ? toset([for k, v in data.aws_iam_policy.lambda_policies : k.arn]) : toset([]) + role = var.create ? aws_iam_role.role[0].name : "" + policy_arn = each.value +} + + data "aws_iam_policy" "lambda_policies" { for_each = toset(local.lambda_policies) name = each.key From 20717083d5167bb62a03770e1da55c8c3e3fc418 Mon Sep 17 00:00:00 2001 From: badra001 Date: Fri, 21 Jan 2022 12:23:08 -0500 Subject: [PATCH 07/33] remove role module --- version.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.tf b/version.tf index 0d48594..530f255 100644 --- a/version.tf +++ b/version.tf @@ -1,3 +1,3 @@ locals { - _module_version = "0.0.2" + _module_version = "0.0.3" } From ffdd793c530a518ca4d8468e6aaf77bb1cd12d46 Mon Sep 17 00:00:00 2001 From: badra001 Date: Fri, 21 Jan 2022 12:25:48 -0500 Subject: [PATCH 08/33] fix --- role.tf | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/role.tf b/role.tf index 1dfec49..63ccb9d 100644 --- a/role.tf +++ b/role.tf @@ -6,9 +6,9 @@ locals { resource "aws_iam_role" "role" { count = var.create ? 1 : 0 description = "Lambda role for Dynamic Route53" - name = format("%v%v", local.lambda_name) + name = format("%v%v", local._prefixes["role"], local.lambda_name) force_detach_policies = local._defaults["force_detach_policies"] - max_session_duration = var.max_session_duration + max_session_duration = local._defaults["max_session_duration"] assume_role_policy = data.aws_iam_policy_document.lambda_assume.json inline_policy = { From 63d714ee9ea19dad5f1012b33de025d7a3df1382 Mon Sep 17 00:00:00 2001 From: badra001 Date: Fri, 21 Jan 2022 12:26:08 -0500 Subject: [PATCH 09/33] fix --- version.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.tf b/version.tf index 530f255..6f04624 100644 --- a/version.tf +++ b/version.tf @@ -1,3 +1,3 @@ locals { - _module_version = "0.0.3" + _module_version = "0.0.4" } From c2ebc013d0c539ce82daae2718ef432ba446cdb5 Mon Sep 17 00:00:00 2001 From: badra001 Date: Fri, 21 Jan 2022 12:27:11 -0500 Subject: [PATCH 10/33] add --- defaults.tf | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 defaults.tf diff --git a/defaults.tf b/defaults.tf new file mode 100644 index 0000000..71d8828 --- /dev/null +++ b/defaults.tf @@ -0,0 +1,6 @@ +locals { + _defaults = { + "force_detach_policies" = false + "max_session_duration" = 3600 + } +} From ecc3fef2075cf7c0145b67ee1d2cb183b07c52ce Mon Sep 17 00:00:00 2001 From: badra001 Date: Fri, 21 Jan 2022 12:27:53 -0500 Subject: [PATCH 11/33] update --- role.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/role.tf b/role.tf index 63ccb9d..da1db8c 100644 --- a/role.tf +++ b/role.tf @@ -11,7 +11,7 @@ resource "aws_iam_role" "role" { max_session_duration = local._defaults["max_session_duration"] assume_role_policy = data.aws_iam_policy_document.lambda_assume.json - inline_policy = { + inline_policy { name = var.name policy = data.aws_iam_policy_document.lambda_policy.json } From 833059cdb4e3591e0786aa7e8360b12ee8e20013 Mon Sep 17 00:00:00 2001 From: badra001 Date: Fri, 21 Jan 2022 12:40:23 -0500 Subject: [PATCH 12/33] fix --- role.tf | 2 +- version.tf | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/role.tf b/role.tf index da1db8c..af1f1e1 100644 --- a/role.tf +++ b/role.tf @@ -29,7 +29,7 @@ resource "aws_iam_role" "role" { } resource "aws_iam_role_policy_attachment" "role" { - for_each = var.create ? toset([for k, v in data.aws_iam_policy.lambda_policies : k.arn]) : toset([]) + for_each = var.create ? toset([for k, v in data.aws_iam_policy.lambda_policies : v.arn]) : toset([]) role = var.create ? aws_iam_role.role[0].name : "" policy_arn = each.value } diff --git a/version.tf b/version.tf index 6f04624..c6137af 100644 --- a/version.tf +++ b/version.tf @@ -1,3 +1,3 @@ locals { - _module_version = "0.0.4" + _module_version = "0.0.5" } From c1e155aff7b074147041411ba50a99dff499c37b Mon Sep 17 00:00:00 2001 From: badra001 Date: Fri, 21 Jan 2022 12:53:12 -0500 Subject: [PATCH 13/33] update tag --- role.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/role.tf b/role.tf index af1f1e1..80cb1ba 100644 --- a/role.tf +++ b/role.tf @@ -24,7 +24,7 @@ resource "aws_iam_role" "role" { local.base_tags, var.tags, lookup(var.component_tags, "role", {}), - tomap({ Name = local.lambda_name }) + tomap({ Name = format("%v%v", local._prefixes["role"], local.lambda_name) }), ) } From a52ad5c1e6a12fcf769ad4f7f0f68f875221ffdf Mon Sep 17 00:00:00 2001 From: badra001 Date: Wed, 26 Jan 2022 09:36:57 -0500 Subject: [PATCH 14/33] fix --- code/ddns-lambda.py | 3320 +++++++++++++++++++++---------------------- 1 file changed, 1660 insertions(+), 1660 deletions(-) mode change 100644 => 100755 code/ddns-lambda.py 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 From 0ae5856578536e1e71c833d7d60dc61541c7c706 Mon Sep 17 00:00:00 2001 From: badra001 Date: Wed, 26 Jan 2022 10:31:16 -0500 Subject: [PATCH 15/33] add lambda --- README.md | 2 ++ code/ddns-lambda.zip | Bin 0 -> 11080 bytes code/defaults.tf | 1 + code/make-zip-file.tf | 20 ++++++++++++++++++++ defaults.tf | 2 ++ lambda.tf | 33 +++++++++++++++++++++++++++++++++ variables.tf | 12 +++++++++++- 7 files changed, 69 insertions(+), 1 deletion(-) create mode 100644 code/ddns-lambda.zip create mode 120000 code/defaults.tf create mode 100644 code/make-zip-file.tf create mode 100644 lambda.tf diff --git a/README.md b/README.md index dca4c1e..bdb7c13 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ No modules. | [aws_dynamodb_table.table](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/dynamodb_table) | resource | | [aws_iam_role.role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | | [aws_iam_role_policy_attachment.role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_lambda_function.lambda](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_function) | resource | | [aws_arn.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/arn) | data source | | [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | | [aws_iam_policy.lambda_policies](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy) | data source | @@ -38,6 +39,7 @@ No modules. | [component\_tags](#input\_component\_tags) | Additional tags for Components (s3, kms, ddb) | `map(map(string))` |
{
"ddb": {},
"kms": {},
"s3": {}
}
| no | | [create](#input\_create) | Flag to indicate whether to create the resources or not (default: true) | `bool` | `true` | no | | [dynamodb\_table\_name](#input\_dynamodb\_table\_name) | Different DynamoDB table name to override default of var.name) | `string` | `null` | no | +| [lambda\_environment\_variables](#input\_lambda\_environment\_variables) | Map of lambda environment variables and values | `map(string)` |
{
"DynamoDBName": null,
"SleepTime": 60,
"TagKeyCname": "boc:dns:cname",
"TagKeyHostName": "TBD",
"TagKeyZone": "boc:dns:zone"
}
| no | | [lambda\_name](#input\_lambda\_name) | Different Lambda name to override default of var.name) | `string` | `null` | no | | [name](#input\_name) | Name to use within all the created resources (default: inf-dynamic-route53) | `string` | `"inf-dynamic-route53"` | no | | [override\_prefixes](#input\_override\_prefixes) | Override built-in prefixes by component. This should be used primarily for common infrastructure things | `map(string)` | `{}` | no | diff --git a/code/ddns-lambda.zip b/code/ddns-lambda.zip new file mode 100644 index 0000000000000000000000000000000000000000..f0143ac6c9068f7de29648b04af7372690f18276 GIT binary patch literal 11080 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 literal 0 HcmV?d00001 diff --git a/code/defaults.tf b/code/defaults.tf new file mode 120000 index 0000000..aeaa3fe --- /dev/null +++ b/code/defaults.tf @@ -0,0 +1 @@ +../defaults.tf \ No newline at end of file diff --git a/code/make-zip-file.tf b/code/make-zip-file.tf new file mode 100644 index 0000000..cab186d --- /dev/null +++ b/code/make-zip-file.tf @@ -0,0 +1,20 @@ +locals { + lambda_file = format("%v.zip", local._defaults["lambda_file"]) + lambda_code_files = [ + "ddns-lambda.py", + ] + # this gets a sha256hash of each file, and then a sha256 hash of the comma-separated hashes. This will help determine + # to make a new zip file or not + lambda_code_files_hashes = { for f in local.lambda_code_files : f => filesha256(f) } + lambda_files_hash = sha256(join(",", values(local.lambda_code_files_hashes))) +} + +resource "null_resource" "zip" { + triggers = { + lambda_files_hash = local.lambda_files_hash + } + + provisioner "local-exec" { + command = "zip ${local.lambda_file} -j -r ${join(" ", local.lambda_code_files)}" + } +} diff --git a/defaults.tf b/defaults.tf index 71d8828..7168371 100644 --- a/defaults.tf +++ b/defaults.tf @@ -2,5 +2,7 @@ locals { _defaults = { "force_detach_policies" = false "max_session_duration" = 3600 + "lambda_handler" = "ddns-lambda.lambda_handler" + "lambda_file" = "ddns-lambda" } } diff --git a/lambda.tf b/lambda.tf new file mode 100644 index 0000000..f7f5af3 --- /dev/null +++ b/lambda.tf @@ -0,0 +1,33 @@ +locals { + lambda_environment_variables = lookup(var.lambda_environment_variables, "DynamoDBName", null) != null ? var.lambda_environment_variables : merge( + var.lambda_environment_variables, + tomap({ "DynamoDBName" = local.dynamodb_table_name }), + ) + lambda_file = format("%v/code/%v.zip", path.module, local._defaults["lambda_file"]) +} + +resource "aws_lambda_function" "lambda" { + function_name = local.lambda_name + handler = local._defaults["lambda_handler"] + memory_size = 128 + reserved_concurrent_executions = -1 + role = aws_iam_role.role.arn + runtime = "python3.9" + source_code_hash = filebase64sha256(local.lambda_file) + filename = local.lambda_file + timeout = 30 + # version = "$LATEST" + + environment { + variables = local.lambda_environment_varaibles + } + timeouts {} + tracing_config { + mode = "PassThrough" + } + tags = merge( + local.base_tags, + var.tags, + map("Name", local.lambda_name) + ) +} diff --git a/variables.tf b/variables.tf index e1c9ca6..ee06181 100644 --- a/variables.tf +++ b/variables.tf @@ -16,4 +16,14 @@ variable "lambda_name" { default = null } - +variable "lambda_environment_variables" { + description = "Map of lambda environment variables and values" + type = map(string) + default = { + SleepTime = 60 + DynamoDBName = null + TagKeyCname = "boc:dns:cname" + TagKeyZone = "boc:dns:zone" + TagKeyHostName = "TBD" + } +} From 4591656b3c0c665cb476f4ba984738db8e44571f Mon Sep 17 00:00:00 2001 From: badra001 Date: Wed, 26 Jan 2022 10:41:19 -0500 Subject: [PATCH 16/33] update --- dynamodb.tf | 1 + lambda.tf | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/dynamodb.tf b/dynamodb.tf index 0a83d84..918a220 100644 --- a/dynamodb.tf +++ b/dynamodb.tf @@ -3,6 +3,7 @@ locals { } resource "aws_dynamodb_table" "table" { + count = var.create ? 1 : 0 name = local.dynamodb_table_name hash_key = "InstanceId" billing_mode = "PROVISIONED" diff --git a/lambda.tf b/lambda.tf index f7f5af3..3c158d3 100644 --- a/lambda.tf +++ b/lambda.tf @@ -7,11 +7,12 @@ locals { } resource "aws_lambda_function" "lambda" { + count = var.create ? 1 : 0 function_name = local.lambda_name handler = local._defaults["lambda_handler"] memory_size = 128 reserved_concurrent_executions = -1 - role = aws_iam_role.role.arn + role = var.crate ? aws_iam_role.role[0].arn : null runtime = "python3.9" source_code_hash = filebase64sha256(local.lambda_file) filename = local.lambda_file From 471a41b902540f9fdb1f2bf047502a0a66ef94ac Mon Sep 17 00:00:00 2001 From: badra001 Date: Wed, 26 Jan 2022 10:49:19 -0500 Subject: [PATCH 17/33] update --- role.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/role.tf b/role.tf index 80cb1ba..a1984e7 100644 --- a/role.tf +++ b/role.tf @@ -78,7 +78,7 @@ data "aws_iam_policy_document" "lambda_policy" { "dynamodb:Scan", "dynamodb:UpdateItem", ] - resources = [aws_dynamodb_table.table.arn] + resources = [var.count ? aws_dynamodb_table.table[0].arn : null] } } From 5ceb2ae802aba3f3a8462834851af68064216ed0 Mon Sep 17 00:00:00 2001 From: badra001 Date: Wed, 26 Jan 2022 10:52:22 -0500 Subject: [PATCH 18/33] fix --- role.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/role.tf b/role.tf index a1984e7..d4bd1f0 100644 --- a/role.tf +++ b/role.tf @@ -78,7 +78,7 @@ data "aws_iam_policy_document" "lambda_policy" { "dynamodb:Scan", "dynamodb:UpdateItem", ] - resources = [var.count ? aws_dynamodb_table.table[0].arn : null] + resources = [var.create ? aws_dynamodb_table.table[0].arn : null] } } From 4ebe2e7c9693112e1a1af69c8b7ac76e75bc40b6 Mon Sep 17 00:00:00 2001 From: badra001 Date: Wed, 26 Jan 2022 10:56:17 -0500 Subject: [PATCH 19/33] fix --- lambda.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lambda.tf b/lambda.tf index 3c158d3..0f1b29c 100644 --- a/lambda.tf +++ b/lambda.tf @@ -12,7 +12,7 @@ resource "aws_lambda_function" "lambda" { handler = local._defaults["lambda_handler"] memory_size = 128 reserved_concurrent_executions = -1 - role = var.crate ? aws_iam_role.role[0].arn : null + role = var.create ? aws_iam_role.role[0].arn : null runtime = "python3.9" source_code_hash = filebase64sha256(local.lambda_file) filename = local.lambda_file From f394ca543b2af81dc6c36e29263930956d5f590f Mon Sep 17 00:00:00 2001 From: badra001 Date: Wed, 26 Jan 2022 11:07:04 -0500 Subject: [PATCH 20/33] fix --- lambda.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lambda.tf b/lambda.tf index 0f1b29c..6efc48c 100644 --- a/lambda.tf +++ b/lambda.tf @@ -20,7 +20,7 @@ resource "aws_lambda_function" "lambda" { # version = "$LATEST" environment { - variables = local.lambda_environment_varaibles + variables = local.lambda_environment_variables } timeouts {} tracing_config { From d77d6b50763d074faadd9a32c2f5f35f482eddc1 Mon Sep 17 00:00:00 2001 From: badra001 Date: Wed, 26 Jan 2022 11:11:41 -0500 Subject: [PATCH 21/33] bump version --- version.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.tf b/version.tf index c6137af..0dd68c9 100644 --- a/version.tf +++ b/version.tf @@ -1,3 +1,3 @@ locals { - _module_version = "0.0.5" + _module_version = "0.0.6" } From da0b152296b2bf96c91c283bbf1a827f17894f76 Mon Sep 17 00:00:00 2001 From: badra001 Date: Wed, 26 Jan 2022 12:34:10 -0500 Subject: [PATCH 22/33] add cloudwatch stuff --- README.md | 5 +++++ cloudwatch.tf | 47 +++++++++++++++++++++++++++++++++++++++++++++++ defaults.tf | 3 +++ lambda.tf | 11 ++++++++++- version.tf | 2 +- 5 files changed, 66 insertions(+), 2 deletions(-) create mode 100644 cloudwatch.tf diff --git a/README.md b/README.md index bdb7c13..815aa0c 100644 --- a/README.md +++ b/README.md @@ -19,10 +19,15 @@ No modules. | Name | Type | |------|------| +| [aws_cloudwatch_event_rule.ec2_rule](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_event_rule) | resource | +| [aws_cloudwatch_event_target.ec2_target](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_event_target) | resource | +| [aws_cloudwatch_log_group.log](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudwatch_log_group) | resource | | [aws_dynamodb_table.table](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/dynamodb_table) | resource | | [aws_iam_role.role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role) | resource | | [aws_iam_role_policy_attachment.role](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) | resource | +| [aws_lambda_alias.lambda](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_alias) | resource | | [aws_lambda_function.lambda](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_function) | resource | +| [aws_lambda_permission.allow_cloudwatch](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/lambda_permission) | resource | | [aws_arn.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/arn) | data source | | [aws_caller_identity.current](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/caller_identity) | data source | | [aws_iam_policy.lambda_policies](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy) | data source | diff --git a/cloudwatch.tf b/cloudwatch.tf new file mode 100644 index 0000000..596183b --- /dev/null +++ b/cloudwatch.tf @@ -0,0 +1,47 @@ +locals { + cloudwatch_name = format("/aws/lambda/%v", local.lambda_name) + cloudwatch_event_pattern = { + "source" = ["aws.ec2"] + "detail-type" = ["EC2 Instance State-change Notification"] + "detail" = { + "state" = ["running", "shutting-down", "stopped"] + } + } +} + +resource "aws_cloudwatch_log_group" "log" { + count = var.create ? 1 : 0 + name = local.cloudwatch_name + # kms_key_id = var.kms_key_arn + retention_in_days = lookup(local._defaults["cloudwatch"], "retention_in_days", 7) + + tags = merge( + local.base_tags, + var.tags, + map("Name", local.name), + ) +} + +# aws events put-targets --rule ec2_lambda_ddns_rule --targets Id=id123456789012,Arn= + +resource "aws_cloudwatch_event_rule" "ec2_rule" { + name = local.name + description = "Capture EC2 Events to hande dynamic Route53 registration" + event_pattern = json(local.cloudwatch_event_pattern) +} + +resource "aws_cloudwatch_event_target" "ec2_target" { + target_id = local.name + arn = aws_lambda_function.lambda.arn + rule = aws_cloudwatch_event_rule.ec2_rule.name +} + +resource "aws_lambda_permission" "allow_cloudwatch" { + statement_id = local.name + # statement_id = 45 + action = "lambda:InvokeFunction" + function_name = aws_lambda_function.lambda.function_name + principal = "events.amazonaws.com" + source_arn = aws_cloudwatch_event_rule.ec2_rule.arn + qualifier = aws_lambda_alias.lambda.name +} diff --git a/defaults.tf b/defaults.tf index 7168371..401081b 100644 --- a/defaults.tf +++ b/defaults.tf @@ -4,5 +4,8 @@ locals { "max_session_duration" = 3600 "lambda_handler" = "ddns-lambda.lambda_handler" "lambda_file" = "ddns-lambda" + "lambda_timeout" = 300 + "lambda_description" = "Take EC2 Events and register/deregister from Route53" + "cloudwatch" = 180 } } diff --git a/lambda.tf b/lambda.tf index 6efc48c..b4486e0 100644 --- a/lambda.tf +++ b/lambda.tf @@ -9,6 +9,7 @@ locals { resource "aws_lambda_function" "lambda" { count = var.create ? 1 : 0 function_name = local.lambda_name + description = local._defaults["lambda_description"] handler = local._defaults["lambda_handler"] memory_size = 128 reserved_concurrent_executions = -1 @@ -16,7 +17,7 @@ resource "aws_lambda_function" "lambda" { runtime = "python3.9" source_code_hash = filebase64sha256(local.lambda_file) filename = local.lambda_file - timeout = 30 + timeout = local._defaults["lambda_timeout"] # version = "$LATEST" environment { @@ -32,3 +33,11 @@ resource "aws_lambda_function" "lambda" { map("Name", local.lambda_name) ) } + +resource "aws_lambda_alias" "lambda" { + count = var.create ? 1 : 0 + name = local.lambda_name + description = local._defaults["lambda_description"] + function_name = var.create ? aws_lambda_function.lambda[0].function_name : null + function_version = "$LATEST" +} diff --git a/version.tf b/version.tf index 0dd68c9..f6d2caf 100644 --- a/version.tf +++ b/version.tf @@ -1,3 +1,3 @@ locals { - _module_version = "0.0.6" + _module_version = "0.0.8" } From 1311b7f442f5f6efc5f5967a69356f127964a55e Mon Sep 17 00:00:00 2001 From: badra001 Date: Wed, 26 Jan 2022 12:39:23 -0500 Subject: [PATCH 23/33] fix --- cloudwatch.tf | 2 +- defaults.tf | 4 +++- version.tf | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/cloudwatch.tf b/cloudwatch.tf index 596183b..2c6fd15 100644 --- a/cloudwatch.tf +++ b/cloudwatch.tf @@ -27,7 +27,7 @@ resource "aws_cloudwatch_log_group" "log" { resource "aws_cloudwatch_event_rule" "ec2_rule" { name = local.name description = "Capture EC2 Events to hande dynamic Route53 registration" - event_pattern = json(local.cloudwatch_event_pattern) + event_pattern = jsonencode(local.cloudwatch_event_pattern) } resource "aws_cloudwatch_event_target" "ec2_target" { diff --git a/defaults.tf b/defaults.tf index 401081b..e1ee4cd 100644 --- a/defaults.tf +++ b/defaults.tf @@ -6,6 +6,8 @@ locals { "lambda_file" = "ddns-lambda" "lambda_timeout" = 300 "lambda_description" = "Take EC2 Events and register/deregister from Route53" - "cloudwatch" = 180 + "cloudwatch" = { + "retention_in_days" = 180 + } } } diff --git a/version.tf b/version.tf index f6d2caf..9818f37 100644 --- a/version.tf +++ b/version.tf @@ -1,3 +1,3 @@ locals { - _module_version = "0.0.8" + _module_version = "0.0.9" } From ca7ac2300ba6916645dd88d9bba44067166ed18c Mon Sep 17 00:00:00 2001 From: badra001 Date: Wed, 26 Jan 2022 12:43:06 -0500 Subject: [PATCH 24/33] add create logic --- cloudwatch.tf | 13 ++++++++----- version.tf | 2 +- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/cloudwatch.tf b/cloudwatch.tf index 2c6fd15..7432945 100644 --- a/cloudwatch.tf +++ b/cloudwatch.tf @@ -25,23 +25,26 @@ resource "aws_cloudwatch_log_group" "log" { # aws events put-targets --rule ec2_lambda_ddns_rule --targets Id=id123456789012,Arn= resource "aws_cloudwatch_event_rule" "ec2_rule" { + count = var.create ? 1 : 0 name = local.name description = "Capture EC2 Events to hande dynamic Route53 registration" event_pattern = jsonencode(local.cloudwatch_event_pattern) } resource "aws_cloudwatch_event_target" "ec2_target" { + count = var.create ? 1 : 0 target_id = local.name - arn = aws_lambda_function.lambda.arn - rule = aws_cloudwatch_event_rule.ec2_rule.name + arn = var.create ? aws_lambda_function.lambda[0].arn : null + rule = var.crate ? aws_cloudwatch_event_rule.ec2_rule[0].name : null } resource "aws_lambda_permission" "allow_cloudwatch" { + count = var.create ? 1 : 0 statement_id = local.name # statement_id = 45 action = "lambda:InvokeFunction" - function_name = aws_lambda_function.lambda.function_name + function_name = var.create ? aws_lambda_function.lambda[0].function_name : null principal = "events.amazonaws.com" - source_arn = aws_cloudwatch_event_rule.ec2_rule.arn - qualifier = aws_lambda_alias.lambda.name + source_arn = var.create ? aws_cloudwatch_event_rule.ec2_rule[0].arn : null + qualifier = var.create ? aws_lambda_alias.lambda[0].name : null } diff --git a/version.tf b/version.tf index 9818f37..8c978d6 100644 --- a/version.tf +++ b/version.tf @@ -1,3 +1,3 @@ locals { - _module_version = "0.0.9" + _module_version = "0.0.10" } From a2b689ad3b6b1bf042d849e059ed1ea42d5d384d Mon Sep 17 00:00:00 2001 From: badra001 Date: Wed, 26 Jan 2022 12:45:53 -0500 Subject: [PATCH 25/33] fix --- cloudwatch.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloudwatch.tf b/cloudwatch.tf index 7432945..33baf71 100644 --- a/cloudwatch.tf +++ b/cloudwatch.tf @@ -35,7 +35,7 @@ resource "aws_cloudwatch_event_target" "ec2_target" { count = var.create ? 1 : 0 target_id = local.name arn = var.create ? aws_lambda_function.lambda[0].arn : null - rule = var.crate ? aws_cloudwatch_event_rule.ec2_rule[0].name : null + rule = var.create ? aws_cloudwatch_event_rule.ec2_rule[0].name : null } resource "aws_lambda_permission" "allow_cloudwatch" { From 419aa9555ac2d8dfb9ab5d2575ab8c7034f6aac2 Mon Sep 17 00:00:00 2001 From: badra001 Date: Wed, 26 Jan 2022 12:54:08 -0500 Subject: [PATCH 26/33] update code: add version --- code/ddns-lambda.py | 6 ++++-- code/ddns-lambda.zip | Bin 11080 -> 11106 bytes 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/code/ddns-lambda.py b/code/ddns-lambda.py index d9f46af..c43675b 100755 --- a/code/ddns-lambda.py +++ b/code/ddns-lambda.py @@ -39,6 +39,7 @@ LOGGER = logging.getLogger() ACCOUNT = None REGION = None +VERSION = '0.0.2' # Adjust the logging level [logging.INFO, logging.DEBUG, logging.WARNING, etc] LOGGER.setLevel(logging.DEBUG) @@ -51,7 +52,7 @@ TAGKEY_ZONE = os.environ['TagKeyZone'] TAGKEY_HOSTNAME = os.environ['TagKeyHostName'] -print('Loading function ' + datetime.datetime.now().time().isoformat()) +print('Loading function v%s: %s'.format(VERSION,datetime.datetime.now().time().isoformat())) def lineno(): # pragma: no cover """ @@ -1658,4 +1659,5 @@ def get_subnet_cidr_block(client, subnet_id): # Message=str(message) # ) # except ClientError as err: -# LOGGER.debug("Unexpected error: %s", str(err)+lineno()) \ No newline at end of file +# LOGGER.debug("Unexpected error: %s", str(err)+lineno()) + diff --git a/code/ddns-lambda.zip b/code/ddns-lambda.zip index f0143ac6c9068f7de29648b04af7372690f18276..051068d1989490615a9079f5a9e064a2f7bf9740 100644 GIT binary patch delta 5726 zcmV-k7NP0LR^nEFP)h>@6aWAK2mr5UI#eVSg-Hx5008R{0RRpF8~|iwZgVYcVQpe$ zVJ>iaRa6N81DcBQVW5ifVRd*5009IO1ONa8fCT^m?LBK#Al z{XiVMfdS8OJ7x&LcHA;Vhv*idoo=Z^QZvIj{NFD#>!Eso>d`cK_6~)^LrYazSyfs2 zs;sPSc9FALzO~KXj9HPyGd5k7RlZ<%d0DY6UL@=$P3ALJeAMvYZ{9?EiGc@`C;2!^sh=;@grTq@-eb zMwOH4w=B)dD$b_K4vVuH)T#hs1Q*LL&F-d4mM;-B*4^ELE}v`I%yO)C2Xz|Sjf(^; z@38AtML(Jw0m#+eR!qa0%-H|rS;D3;&hTT0eZEV7r*~`-SJS(sq<+luMVw|d?qfu6 zKV#8;1QRpOiy8J=)Wzw*^2euJ-$E%ConD*`oMN&RP6>jfZpETk&>Jy`)Y1XfDC^Hw z)o=A8(3)&al%q-lz8owsc%wr`FKX~cZ)`-6$kN3!FDmxWGSB4S&v-6~?kYL`<^Xqudeu;}TzMd!L*82}{-yWT@J&EA>HmTmj zpGh$sZS5Z%eE9j~0=}LA1#O)jy*>VLB7gmVa&&gie@3szugBj;*v|g!pFof_bOIoo zC-=#mT}t@JCqI7JQLP;w{qXbK`sc6vXD7!e@FJUG@@(kI@QFhPnWhdFz`TL`z(~Dyuuss;qYM7VfB)dfRT@`M!-jZ zejqyBV|tOp2cUr*KE+sS>tg@y-;e%#a^NX>5#Rnjc{s?7f<&3Va{mC7>tgRdoL>Y% zzJrx4n%`O$*oWwS9?x{f#qPf;ciA^(G)C?gS3@yFJCZWT>W?h{JRFVjPxvP-b5UV5 z8f|UOk{dQpvn0!hqg}?f*|Lal7x6BCa;7OV-z^3Yt_S@0EU8vS#@WbpRTMyURGVe1 z#WgT$U@Q5m5bT#~>=smI*8s!cv9c6MYaXVl0}uDPltZW^x@*Kfym7 zY3#x_!%hGtPs*&EOlc`VXW+eTlmGP=I3<)|yo7kE@Uu%-#e;mQp-c;NEZT^F09rH> zh4`Y>S0akAq$qSKd@ct+XUP}r9Pro#OX55Z*f;n?ObPa;$X8YJ{dZcA^fAQVh!V0# z!SSZPH1enJ6V zCJ87r39ud*0w$R>=m5apg{>GB?S!up^Ndky}S%U)K_E+lM@jU{MT36!|J z67`4 zA8ol^gGYYP|+7)&5AcKyKO&!8DpLNgI%Xj$@cw1-h^NRj|> zY^5%f9tJ-Su;hy576y-7o@n_@!I+b41{;4Z2GLwRo#v~o5)E*K>#zK`__(d?3;x=J z691rK!(Vtu>_awx0K!8uEAvG{7B;SjdQM@H7ON~nvrMcNF_&P{h!2rN-vW~otp*}t zkiUO<4YCM0%&g>?S-1$`gXIIT?rMN55$wh>P~-}it|}ua7dejL)+kD#bj+|KB5Qw? zB~_jf8NR94jJYZen1c-U{Q3N05Q3u&B@J~>d^I&>+**(`q*EHq9ZGN|EQdlY?C7*O z(jcED3ot2|Vy`eyF<2>az24*}eI%nZ@$x58Ji;-RO_-gRZ^z4gIdm&}kjYl_IdFsd z3V4=-2FUUM20=ZIGh}BaN?Bm^>9l|P^M9RxIMIO3^63h>6k?{hA>f`?BwJ&^OK&Ma zW~xu_0(4iOH4_DOXOb+I)x!ikGC@4+`c9q6Y_(WIMRahC@$Yh2d$&Mcj92q&4?j-I z1bF4#13^ptFr!^Np}jR9*Tt=4rE61Ut73v#wdz#;&h8@?`T1s?v3VXeo8 zs}(-OSV%7=0yd020=cJ}xI}B_loa>yEw`e7-5{tQ&`>dlii z3nB@zK9gmU+@xQ`lVA%Z5UrMEotxP64-+!m_d=ce6O)t+AR{?FU@6~T1g5;kD*I(Q zr6nd{?iW0&>7C;0D39>X`D#g_LzCAFHVwA$40kEn_uokh*MsJh8VoH1hz-1xN({sT z3UreY4MYp*cV=+h%jg~b!tj${4JQG#lZOpf1hUJ%fRo`36$n%MI2GCj--wg`4H*Y~ zxOi5qe~gnJ4m=f~I+z}OpiXjrtOy6nBXD>06FhYGlWz_}2O6gsz`&0!lei8t0{OX< z><$?i*Mu$9=6KAgq!XecClfu}7!;JJStU<U(UQCjuW zc@m>JKAY92_LF=Maxd7(Lpwq`=r%l701EsHYS8-OG!7Zh{tctQClApM`|bD9cn%=a z<#04!Ethy6aFZPn9svcDJP};aG_)Z(-i_mSZ^ zT4l+XlYtp>0jrbr86pS{hCdC(t^N^{9vU1$4=lbxkEeFHy$-EWAvOQz4XF`qAgz)G zzxsGaFlX13D!!g4L-lhyPm`?LVTXr5U=VqIqcue;D0udQfKo|O4U>BsXaNnA{N1SV`;&egUlAWn@g(ja&Baje{gLczoBf!R?i_>yKg*MJ9i13e zP*FKti)c!dR5nW^Wg}(i1uvs_^y?p!DIRzt_PaiS)^Dt@ zTuOQ}bEbF?;5DN@R@6DIcTYM*}pR z3@}Z0`9Yj9Eq~H1D`lkbbux`He?Mb>!)B``v=*B?p^YIIG7#%f*0jf>-t3`JFx4X* zwzD%_m|s&j4m9i>|eq&wC&+Mro2OFk`C4pbkKP|$9KD!^4bQ)XY4~`{kTWv5dLJ$Q zx{5y(S0^AJe#9i>ouvw&KN2iFJGoH&4FTcF-d|&C1<&z=G1@XUP@#i35hkOL%ev7& z1lvvc>jjH41tVv$a`zWs?SG-SvQhYPyf`{^<$>CsFboV&FI2~q@UF5 zX;7F55J6v zd3}+5i)M$DJ|H|AJuQh!fM%4EeG`(9r1nKfU4ZO3#T0-`!M3A^pUIHM{G&YW&w+$vQ@a z6v!)N=lJHpsGza;KDS)8-f$&>cE96DA1pnx%qVxs&oDdk-NnCG=(|ZUQs8PaVsvo& zN+i>162U=5DKI{|IHO|EAgNTA7SIHT>5~4n*7$!JFT$5KXdNJu#In||!#X}?BC(3P z73(gUFH4*Q>uLAf+uQ7%U=xyr!0BVRxnty3=zw?L)@fn-Fodu#VLPu zOLsN)i?=jrB+@q5UEP4TT*fNz+SnJwOiu(wBa7`An2&bMxsgwuQQoRxSdeC`{t7z( zG$E0F*%lJ7`LFE2c#7iVK_Pfa&=0>XbeJ@5x?!-q4Pz-U*@q&Hfd-%nVd5xG0D|s1x@i?mvPO_hdsWsAD>&I~;io3Jsxqau$YxcR*gV6D{N~gK!CF z3t=ys?I>P3C$lV&Pc7E1t1^G3D7P{sGwX&-S2W7IL4opo`3B@w3LWIQ*>tm@HctsY z<~&zD4hRj=Z4zWgSb?2Q`e~#V?_{eS;sRr@gSdEl@gU);y#!1PF5SfCB*~EH&zcXA z{wg*PFgi(9MGw2E-QsBNcYw6PjY^k-$L&&9}to znPywUt6d5@FVwDGx8nRfhYpQUJXNt}+RdoDe$Igi&?R%t-?9-B9U1$;{z>uo+{}fg zOO!euE#P#B{17#K`~`o)v8QK(V17!f7^FiSS>XnHD+WRP%Z!)#a_9ne=+s~PnF&)k ze5UEFm|V~E=_f^#;)|Cm>D#noSdCG&7K&8BHo%KGENjz_yR!oOb+U2ei*J+8;UFBW zG=OH!Uk z5|ITZV3^*dfaCeeIlNq^(+;YrK%k2bs!sg0D50i+M`UD8^btw7yy&MEc;-LwD>F4x z=bbbJ*vtJQ+lykrB>r6P!r<=8131DJhkh6JPV|?*M16684vV#PJTX6U{PuKQA9>1M z42XMRC=`je^}hsAO9KRx;3^oCRw_IVuVy+_Bou{73@QKs>yw`)R=P)h>@6aWAK2mpjkI#fzxYc%pH006=e0RRpF8~|iwZgVYcVQpe$ zVJ>iaRa6N816N$}VOU)8VRd*5009IO1ONa8fCT^m?LBK#` zejt|Jz<_7C9Ww-AJ9ZhOLv#z!PPfz{shQy%{_mHW^-w*3^=KMAyNANzp{1&ysTIjFA{c>Ci5Aqa<(cHR^7!Fi%WKRa?bMm zq+rLVES}AZq%60#27|%YA%1xmFRo`XJAJ6`^6Xo7J}uH^wY7C|mzJ!gAM8^)pR;9B z+~maqph7c$`Fx&#O0!#bv&yDbnrG#BYb(8BSN@omWvQc|%z zqsqzj8+Wtrm(Mk9W;xcngE|fE#zlga zci8o+q94tT0Oaa!E2iO0X6%3SEMZd^XZW$hKHa5%(>u0^tLa@*Qa@(-B2F_J_c5Zk zpRs5^f{B^t#SHr_>f&@@`Qy{AZ=e*5PA|>|PBB>urvyP#w_?#N=#3aeYUzM#l=Ww; z>Nk23XiYXI%26c&Uk;WRywM?}7d3dJH#Q9v@}ZP}&Hzl6LQ(O% z0D8!O7xLFUzrDpNmLFBI)heCIzxC=sSIcBt$)DGGm47RL)rb|Z;sB`oi+CsKz=k(qB@%=_sN`HN@&L?KfK>j zun&*E|LINr^Oyazlj9S3kyO(wfyo%A?j6Dy8kI)ahm-Tk!Mo$58XZ zOKj`>-OkO%*%0--KRyKT}J2gBw1d-I7e3_;2Pf()$K98$l(Lbcn+U` zVl1_FvH#}pNB@`{cuHQxw|`F_4l<)4(V?&0KLO>s*xUE#7eSD3Vcm-6x0VI=A$pg` zGo4Mbi2YH~+*tjQ<)4P5G5!hvq-BnbF0O{7(bm>1xnc7(OR{`8+GT8;EsOYe5$__) znIc!+V(=iLRD-T(`P z5{#D+FBN`v>8f~;FEx~D#f?Q95kQMZq7Ywg`VvFgloW*yg-_eyr!4u5odf27nPACX z_EkAxU*QiiCD@xHUscI>-)cS5#}IoXO2{4s$D8`n*b@N>dehREwR!>~v7r|zFdyFo zfbvAx&uHn-t{b`_i)^4D64VoPW=^Wmj?b?S(Y}d)`6O z>o1l-O?&pt*ChnC?fIyD%NpK)gS%w<5hZV#B%q}vzPG+=0Kdc#aC=0}NMM zR^^znV^o?zH4@rK3ZfsOk=n>pqXDWBgc{jKCm=3RWBuHV(>Zowhn1v|Ja7b|BiEq2CaA*A80iAYr>VER_q)p&*eIXx zUHy%ia^syf!&Z5kV&k2dNaJ12kaxptLdf_uyUB+GjvJEh4v7K!Jp!2^h1AHcBeAb* zi8pRNOJS(-+oI3Ycn&LnV*)0`64a&yN?cxvdPIjPYhjod@kh|SR|VQ2NkOC(*F1oe z7s~$`3PhX()J3Fv#rXi&Nd594hor^HciTv73m6R; zOdv3J{lMZ+lOYBb0U47w20wo=(Te0229KMRXwpo5UNh#ZG++)gRP*Qa zhd~IAvWYa*Iq}uhka1%{&X7)NFn1`ym9QKNv9P1l;z)yhmMp-eV2ZuMJjGz7#PxcU zpY)N8&cw?fMezv7R5pKMc3!?6FZ1Qlt>{4}Tg~Ud4dyH0Sq>T?$NvF>dKzcQ&PtTB z!06Ly^{4+ie}AF@o8{9Law)`2aYMj8tw^@UfS2AdqutEUSkJ zc4UHh*7cn_li6yqgo^077~|jNuy$L4x)`tK)gFGFlnL<4xd(rOrlJTqb1+D2IMl?b zrL!6oALFUN`iH;-3i`E>(Ljh^D#jTy7le8STyh90)*lsUa*|fb0#-z=A2-V+W2x-; zMmE&b8M^PFMUt%8Dt656fQhBaEwly>rMp~`wC?tPwxqB7lY>|LP06I5(UDOIZ{1?Moj}0Fxe1@@*?neY{7<&YAPc?Cg z%qc1EY3M*PyZjnt^s(fm;`asA5#o~DfetuOPd$QDdWQ`?@wW8#9X?E|hb2zPEa4Uj zDpLF9s)^dQepsOj^n77=v5=vc6nl4!^13eUQe z$_Y*ay_%C63LFIXl-b9VIto$(BMp;|3RNKYahV*+jeTP)$Z3!hhId;?_|}WUwSmgj zHSk}yh|7Sxln~brBxN2i=^#_Ny5=V5 z_(PL<5t9``d?w)D?Ne8e1nbBv^mwL@Yy%S?lOYlm0T+`t5>x`e?~{xYBn20-6u(fD zwG#3HK9i^uBo{!SJL*1ua07}kOl~v^<$Bb!!IR??LJap#RKPQ1k;^GdK2eh~ z6dVDPlTH*l0eX{&6hH!&9J9(469En$>{R-1U)ZUQ?+ZKq_p|F21pyx4i7BpS(!Mao z$3uQ$icKhWWr|xI#BnK1(U={+J(CU>8xF{}_2)xoSfmy-v>`ualQ$P>0hg1Z7f=DG zlkyjm4Sf=Wg?CGx4un&}eG!w97NO6 zWy$B0ni+Bdi<1o+B7Y2qKMlpL{vOs&TQWLN{=K48fEmHuW(V^m&e&?n4j-V~`Qi60 zzCn+tcDTI`tx+L0fAEIX2sV&b$%0>fJR_L1>q!+~&y%70Ii06TR_(CE!|ySOyuQ(z zA{7)odqF^{q^O2p@3826HJt*X+^pvF2S9O=-@~v9RFkTV>XWV-P60KO{TeVJUTbTi z6&{Y4ADkGAUeMmYKfeeg=pRoC?UJj7_u((>lT90s0XLJ~8(jf+lRF$p6stolQN4Jh zUG{5q4*%B>{LQHF+mn|ZUlH$2@g(ja&Baje{hsV=oBfcJ2OWe0zsi%19i13OP*FKt zi)c!dR5nW^Wg}(i1uvtw^y{CKLLPV``kOw0l_!)49 zwj{Q1_s`$1lXo8?69iL(UZ?-|VHf>Uz;bhnS|xweldK;s4~yndo!O~VXT~ER>erv{ zljk2P8u=*lsm?XludT0KN_sMLrg#tFJV}Xn6!JrGLk zH&3UP$QYGVK1RWh252}LV4CdmgE(Va{-jw}%1GbqWEx|Ce#ZQU%~nfjEjD*T8$&K+ zAl9L*X^%y{*+ZdVsz*3%XJ@!Dzoc#)XxKG$BoHwB*!UHR#ZMLl3NqFxMANM7fcOoI zLQ0=`v&y(fnYUR6lXL2B?`2$;Tp&=WT|A%Tm`YIN=Mkk^|9XkoZuu#86`Gi#twztY z138c-3{>%drH&&07E$FgI@zyS zw8^#tb3ABdW1h+JlKbO$alL&1KbnSzNAHd{e$QhYjy^ofDUBw@Y zs}qn9KVXva&QgWX9|;zoom?pXhJf&7@2@eng6H_b7;TvvsL(;22$RvrW!>oSg6$^! z^@2s2f{`;=x%-Q+_J7b@*(m%ta&&QrJpRhh^~m3}`Y}^mR7%;GW^~~~(ogF3@%zJs zsyacDph^J0um3hhg0=M@JU9|sQfdgasWwi7w}o6AaSJsJBO1aBMvGREuW(-K!^&%h zD>!N>f6);4c_SJP#w?&0UZ*;D_gRC%gP*X9MHTjA1Ud*L8#!jFv}Pz2yzb;75=&_^ z&q4X&Se2JwV{EIvv+f}ciHd{;Pj} zn7z9C_4Ur5zx{3ax4*!<{~~pNeEjC*{n^pM{`rxkK>);|fI6mw^uy01VqTvm-=f*! zqz?#>Mo&wk5}+BSWZ#4&B&mIW5mHy-iCrq?ML)zu{=_rzmpspDTqM$KSjRWD2~PVi zsE)L&IIA7*=2}Q#&YbnMkanZpFGw=F1YN z3~*UZd3=ma8Of8v)Ec$1@DYa|nlY@5ahb#g@YQ1QVFtYU+uz24e_>=~Cr2n}J~&g~ z1=@%4hd+{o-MN0_4fRIl7drvtyF1OSTAtkAF?JI^^)Ofl{LB%d6n})j@51 z(E=%;+Q17M=_NdPs)*sN;x!sCrNSDo%^_(X4Qo`z-i}Q7fN5GJh=$l`iK>2{GXR8R*EiKRhn-Sjj{`@I8y4Ulzrd**B+0};S>N0MgPCOOO$r^|K@KQIQ}7OEP& z@okZ>mcy@~np}*307y?ZG$(z@4)ar%yl5*JELnz#W!xcdQnqfr_M_qyx~01s`^8%t zG!ki>>#lA7g8w_W@_6T43s`tIL- zvgs%sZ2olNtayfc0t1=G^I?AdFH27R&-wck#=oZj3o18?1Spl@UN^ekk$MjieYP z!gTm?C)A1i5%(WKiF>l471S{u(;bdH28D*uJvj@1!@xTrui1$f@|QulgtLXP7tMAQ zubh)v7RaX->(*77Qj}X6l9_cwrYjod-Jn2uzI+4nDuoVm+-$m8P@AU&A9J3o9tVVm z=r##5BdoyACjB&0i+8eB4sn68*Fju7y?BuD)LsIn1($B(a*|}o^JmQmNPiKV2N<2C zs-lN~UDR%IwDvnd+Tcc|OTpynHPer(4z=w?9I3;sd@E745+IX0u_xxTo{50jvEBy% zWpw1Caitu7&$DfWQ(dl>%e<)iPqe7`qNK`=iiRSeQ0j;wG4y%mF?{bk0>d^vQ1I&|u<{mg_Z96r-@R!pwv z`ShcrN%6%?mGo^|F|5X@S_?%gU>o2?9G10d$K6?h{W{sW@x`}E=Wq}XRvNtr(B1EU zvA0hyEw~a&X-&$zNtI7ZZOb;EPV*K1A+=9&tj42$o^l8*W@Wlqf+Z=>BZU=n{ScVTdM Date: Wed, 26 Jan 2022 13:07:18 -0500 Subject: [PATCH 27/33] add tags --- cloudwatch.tf | 21 +++++++++++++++++---- version.tf | 2 +- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/cloudwatch.tf b/cloudwatch.tf index 33baf71..61b32a4 100644 --- a/cloudwatch.tf +++ b/cloudwatch.tf @@ -29,13 +29,26 @@ resource "aws_cloudwatch_event_rule" "ec2_rule" { name = local.name description = "Capture EC2 Events to hande dynamic Route53 registration" event_pattern = jsonencode(local.cloudwatch_event_pattern) + + tags = merge( + local.base_tags, + var.tags, + map("Name", local.name), + ) } resource "aws_cloudwatch_event_target" "ec2_target" { - count = var.create ? 1 : 0 - target_id = local.name - arn = var.create ? aws_lambda_function.lambda[0].arn : null - rule = var.create ? aws_cloudwatch_event_rule.ec2_rule[0].name : null + count = var.create ? 1 : 0 + target_id = local.name + arn = var.create ? aws_lambda_function.lambda[0].arn : null + rule = var.create ? aws_cloudwatch_event_rule.ec2_rule[0].name : null + propagate_tags = true + + tags = merge( + local.base_tags, + var.tags, + map("Name", local.name), + ) } resource "aws_lambda_permission" "allow_cloudwatch" { diff --git a/version.tf b/version.tf index 8c978d6..980343a 100644 --- a/version.tf +++ b/version.tf @@ -1,3 +1,3 @@ locals { - _module_version = "0.0.10" + _module_version = "0.0.11" } From c680098c32bd7d2817ecfab9fea8eb1c0055934f Mon Sep 17 00:00:00 2001 From: badra001 Date: Wed, 26 Jan 2022 13:10:06 -0500 Subject: [PATCH 28/33] remove tags on event target --- cloudwatch.tf | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/cloudwatch.tf b/cloudwatch.tf index 61b32a4..102a381 100644 --- a/cloudwatch.tf +++ b/cloudwatch.tf @@ -38,17 +38,17 @@ resource "aws_cloudwatch_event_rule" "ec2_rule" { } resource "aws_cloudwatch_event_target" "ec2_target" { - count = var.create ? 1 : 0 - target_id = local.name - arn = var.create ? aws_lambda_function.lambda[0].arn : null - rule = var.create ? aws_cloudwatch_event_rule.ec2_rule[0].name : null - propagate_tags = true - - tags = merge( - local.base_tags, - var.tags, - map("Name", local.name), - ) + count = var.create ? 1 : 0 + target_id = local.name + arn = var.create ? aws_lambda_function.lambda[0].arn : null + rule = var.create ? aws_cloudwatch_event_rule.ec2_rule[0].name : null + # propagate_tags = true + + # tags = merge( + # local.base_tags, + # var.tags, + # map("Name", local.name), + # ) } resource "aws_lambda_permission" "allow_cloudwatch" { From 506b7ac0bb711c9bcc288c61de8a5548b6c3155a Mon Sep 17 00:00:00 2001 From: badra001 Date: Wed, 26 Jan 2022 14:45:05 -0500 Subject: [PATCH 29/33] remove dummy file from zip --- code/ddns-lambda.zip | Bin 11106 -> 10942 bytes code/make-zip-file.tf | 3 +++ version.tf | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/code/ddns-lambda.zip b/code/ddns-lambda.zip index 051068d1989490615a9079f5a9e064a2f7bf9740..8147b70e696b7bdca5441e282400a27e2ece73cc 100644 GIT binary patch delta 30 kcmaD9wl8!;nAYS)S|OADwAuI=85kKt7#J8rw15Ny0H6*BBLDyZ delta 195 zcmdlN`Y3Een3fka3l{?jlm}adfEiFih(U%SB_*#|HzzSSDJ4-aFST5+pfWUslYzM? z<6~kS5SLbPGcd9UvoJ8QG_W!-Ob*cs%VA`aW5#8m1l;_F#t Date: Wed, 26 Jan 2022 17:03:41 -0500 Subject: [PATCH 30/33] update --- cloudwatch.tf | 12 ++++++------ version.tf | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cloudwatch.tf b/cloudwatch.tf index 102a381..c1c9613 100644 --- a/cloudwatch.tf +++ b/cloudwatch.tf @@ -38,8 +38,9 @@ resource "aws_cloudwatch_event_rule" "ec2_rule" { } resource "aws_cloudwatch_event_target" "ec2_target" { - count = var.create ? 1 : 0 - target_id = local.name + count = var.create ? 1 : 0 + # target_id = local.name + target_id = var.create ? aws_lambda_function.lambda[0].function_name : null arn = var.create ? aws_lambda_function.lambda[0].arn : null rule = var.create ? aws_cloudwatch_event_rule.ec2_rule[0].name : null # propagate_tags = true @@ -52,12 +53,11 @@ resource "aws_cloudwatch_event_target" "ec2_target" { } resource "aws_lambda_permission" "allow_cloudwatch" { - count = var.create ? 1 : 0 - statement_id = local.name - # statement_id = 45 + count = var.create ? 1 : 0 + statement_id = "AllowExecutionFromCloudWatch" action = "lambda:InvokeFunction" function_name = var.create ? aws_lambda_function.lambda[0].function_name : null principal = "events.amazonaws.com" source_arn = var.create ? aws_cloudwatch_event_rule.ec2_rule[0].arn : null - qualifier = var.create ? aws_lambda_alias.lambda[0].name : null + # qualifier = var.create ? aws_lambda_alias.lambda[0].name : null } diff --git a/version.tf b/version.tf index e6e07bd..4e8c3aa 100644 --- a/version.tf +++ b/version.tf @@ -1,3 +1,3 @@ locals { - _module_version = "0.0.12" + _module_version = "0.0.13" } From 87870122ea476306079bfe41e2095758d89b21f7 Mon Sep 17 00:00:00 2001 From: badra001 Date: Thu, 27 Jan 2022 13:24:10 -0500 Subject: [PATCH 31/33] update code to use fqdn for ptr --- code/ddns-lambda.py | 36 ++++++++++++++++++++++++------------ code/ddns-lambda.zip | Bin 10942 -> 11061 bytes version.tf | 2 +- 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/code/ddns-lambda.py b/code/ddns-lambda.py index c43675b..a70cc45 100755 --- a/code/ddns-lambda.py +++ b/code/ddns-lambda.py @@ -39,7 +39,7 @@ LOGGER = logging.getLogger() ACCOUNT = None REGION = None -VERSION = '0.0.2' +VERSION = '0.0.3' # Adjust the logging level [logging.INFO, logging.DEBUG, logging.WARNING, etc] LOGGER.setLevel(logging.DEBUG) @@ -51,6 +51,8 @@ TAGKEY_CNAME = os.environ['TagKeyCname'] TAGKEY_ZONE = os.environ['TagKeyZone'] TAGKEY_HOSTNAME = os.environ['TagKeyHostName'] +DNS_RR_TTL = int(os.environ['DNS_RR_TimeToLive']) +DNS_RR_TTL = 60 if DNS_RR_TTL==0 else DNS_RR_TTL print('Loading function v%s: %s'.format(VERSION,datetime.datetime.now().time().isoformat())) @@ -153,7 +155,7 @@ def lambda_handler( # Only doing something if the state is running if state == 'running': - LOGGER.debug("sleeping for 60 seconds %s", lineno()) + LOGGER.debug("sleeping for {} seconds {}".format(SLEEPTIME,lineno())) if "pytest" in sys.modules: # called from within a test run @@ -381,6 +383,7 @@ def lambda_handler( private_hosted_zone_properties = get_hosted_zone_properties(route53, private_hosted_zone_id) LOGGER.debug("private_hosted_zone_properties:" " %s", str(private_hosted_zone_properties) + lineno()) + fqdn = private_host_name + '.' + private_hosted_zone_name if state == 'running': found_vpc_id = False if 'VPCs' in private_hosted_zone_properties: @@ -425,7 +428,8 @@ def lambda_handler( reversed_ip_address, 'in-addr.arpa', 'PTR', - private_dns_name + fqdn +# private_dns_name ) caller_response.append('Created PTR record in zone id: ' + @@ -433,7 +437,8 @@ def lambda_handler( ' for hosted zone ' + str(reversed_ip_address) + 'in-addr.arpa with value: ' + - str(private_dns_name)) + str(fqdn)) +# str(private_dns_name)) except BaseException as err: LOGGER.debug("%s", str(err)+lineno()) @@ -462,7 +467,8 @@ def lambda_handler( reversed_ip_address, 'in-addr.arpa', 'PTR', - private_dns_name + fqdn +# private_dns_name ) caller_response.append('Deleted PTR record in zone id: ' + @@ -471,7 +477,8 @@ def lambda_handler( str(reversed_ip_address) + '.' + str(private_dns_name) + ' with value: ' + - str(private_dns_name)) + str(fqdn)) +# str(private_dns_name)) except BaseException as err: LOGGER.debug("%s", str(err)+lineno()) @@ -725,6 +732,7 @@ def lambda_handler( " %s", str(private_hosted_zone_properties) + lineno()) # create A records and PTR records + fqdn = private_host_name + '.' + private_hosted_zone_name if state == 'running': if vpc_id in map(lambda x: x['VPCId'], private_hosted_zone_properties['VPCs']): vpc_associated = True @@ -770,7 +778,8 @@ def lambda_handler( reversed_ip_address, 'in-addr.arpa', 'PTR', - private_dns_name + fqdn +# private_dns_name ) caller_response.append('Created PTR record in zone id: ' + @@ -778,7 +787,8 @@ def lambda_handler( ' for hosted zone ' + str(reversed_ip_address) + 'in-addr.arpa with value: ' + - str(private_dns_name)) + str(fqdn)) +# str(private_dns_name)) else: LOGGER.debug("No reverse zone associated with VPC - skipping creating resource records %s", lineno()) @@ -847,14 +857,16 @@ def lambda_handler( reversed_ip_address, 'in-addr.arpa', 'PTR', - private_dns_name + fqdn +# private_dns_name ) caller_response.append('Deleted PTR record in zone id: ' + str(reverse_lookup_zone_id) + ' for hosted zone ' + str(reversed_ip_address) + 'in-addr.arpa with value: ' + - str(private_dns_name)) + str(fqdn)) +# str(private_dns_name)) else: delete_resource_record( route53, @@ -1239,7 +1251,7 @@ def change_resource_recordset(client, zone_id, host_name, hosted_zone_name, reco "ResourceRecordSet": { "Name": host_name + hosted_zone_name, "Type": record_type, - "TTL": 60, + "TTL": DNS_RR_TTL, "ResourceRecords": [ { "Value": value @@ -1336,7 +1348,7 @@ def delete_resource_record(client, zone_id, host_name, hosted_zone_name, record_ "ResourceRecordSet": { "Name": host_name + hosted_zone_name, "Type": record_type, - "TTL": 60, + "TTL": DNS_RR_TTL, "ResourceRecords": [ { "Value": value diff --git a/code/ddns-lambda.zip b/code/ddns-lambda.zip index 8147b70e696b7bdca5441e282400a27e2ece73cc..09d2410dd578c9d7389e524c8326d516c8ac8535 100644 GIT binary patch delta 11011 zcmV+eEBw^HRkc=sP)h>@6aWAK2mtPCJ5+eX{jGy40052?0RRpF8~|iwZgVYcVQpe$ zVJ>iaRa6N810Un^VJ+kGVRd*5009IO1ONa8fCT^m?LBLA+eVV#^(*G6OM$G=Tsyhk z2i0k}6iKnQ>&PxiN$OTsu^0Lsy&^53t!=Ye^D;z3c4cTcEPECMq{84LzHNBHG!yu6;r?EOP^muKIy^I4Ius-2ySyR>8_{a~Nc#e%Jp z;wCSa02P{l$rp?KQ<~kfn{_s;(mX3CJ3HwO%W`&jdhp{h%M14Rcc;g!if>DXkdlh! z8C6bZ->@_*t2mn_V-{y~s8s>N2riagn%~VandeyR4(c?t8y5*y z9<%FpML(Jw0m#+ePE5m@%-R3uS;A&8&hTT*KHa5%vpcqotJz&rQa|STGEOrZ_X(nR zkg@0>f{B^s#T@%A>f&@@`IGlM-#{rAy}vjcIK^ZsoDu{{-HJu8pf_R=sii}zQP!WW zs^92Epf%Z;C`Xk9d^uQN@J5G>Uew@?-q?sBk)_L3UR3NKWuD2u%ZF0_IR`LR3Pr{5 z0_Y)sU&>#L{Pq^7SbkK+*6VaG|JJJmU9FN?C4XM$RsOB~k(WC+1bS^YPg^{~5iUyqtU+VLJ!&e*i(!&b}8YXoc{1`tXexd{{E-e_0L}p&Q4EG;YCu-t^{rqn9R2bV`x+wVXsclr-yG( zj%yU*?u~>7l3OMST0Wh`0TypsDSyHWwjB~2lx+s9IsW!{j%WIGYz`62uAviqM z*ej^Yo&koxV`WtgBlbdvu*V|yHGL3^ZX%kwDPltZW_lgZKEgj7Y3#x-!%o0|Vwsj% zIi1mJgU-Nv*(U$%H85i+!FUPrQsHNpu8IfwQbU>6{zSA90kmi&3h{NXFLsoYNm1xf z_*@Qt%93BPbHK(EEQ#|tU|-=6F(ufWB41a@ci(C~(#H^cBTC2~1;?BE(%2IL33}7g zmyLP?!nvUrD7qit1Ay{G*w2Z7<;<@ex*&^ep&t^|6Le-lO4;-dq~QXDiUI%}J*&p* z=Pb{vs7-ov|O_7Qb}GD$#RN`Upi5HKkR zK+OQT!r}$0t`9I=X<3yM#!gUu10_x9K`AI92#pj_UIqRDeU{XVk05q`0H)A4@*AZ| zmR#Mvi|-S5og|smz6e&l$blcDdWZZl1+1#7NI@%15CLil{>u^oQ^v&u0GP$7K|?1p z_K>exl@!Y~L-ar?l1&omv??`dq-6p>#`duD_i?(wE{s`8s?h^S5IS-VT6BWS|aHIx)YN^#8tIC-JoF-L)jbAURKRP;C>;Mx%AdO)vJ{%dGejreHC?J6&+^_0I5 zy+dU}XieBKeL}4T{zZkQjIL^~p@WSYSCiyKpqO;4KF^_RXns-sG5%BOstc(tV~Og> z=7*#`%6Gd+YfBgn7)&5AcKyKOPm>`A6@QV6cgV$YreG1mdQ^+&7|9O@I*FH5l7HXl zYv?kXClxHl=;|_}57CuAW%J~EeLEb;mCfsFb4R~T8nv)dRZ}n)i{ustj~lmWT+P5< zPG~^+#zK`__(V~7=QlS zgA)IsVZ&c|N9?= zJ%>V0jCJB1X^_v7CD<5D@mrX<7|fQqUV!qGK9bRyc=@9!9^shECd|&ux06-A8oCud z$Ykrq0@%Z14NS~I0|fhjgXEsY8S=Lh#V&CEbXNW8ujlVhHDL36wnnyvn13nu2)L&e z3ELR((pw79o$8ajBHiU^%|t=nnI_9s^)SVbOcBqzzEfv9UoTfs5uGX%{JR|19yQP( zcSDp|scsDBmaW+7!Pm9cMRLp`0NXAxQ?QH!l&$J`E>Seo2om;Hd+ zBd<-gFG)AhCKadld;?1mlv?Cz-N!p{xHz?LFko$@)w+&C@|SylLGTs{mMG_}i@Ahb^;Cw~>cFQAT)pWHBX zz=6K%k)+abZ77bnrMK_!VOl+`a6;w@H&IZt;xBnMp~AM71Dpk2&A{lq<;IrK@?uhb z$%LU@2OREKWySd+HE|WIP!pf;xk`kI2dNYG zhuYFG3%iV9gBdj0brb1X&ABrf=AjO~8REdKZApU;RORKx)+YlIu`~32Tqegd=D^q+ zavJ1>;oT<^&i0~kZJ=^}4g8la~#^#P5z&P<4N)eCN*1 zES(qA>wiTK1kCM7QMP_x`=V+vo_6aB`gVeP>9#DO(eUsXfV#A#f>PcMGdyd^N;HlS?4D9{C~rg%=Z0Ir@ls`>;(?>BraC*By#s< zF8qxVqpfHsP&r7vJ<{+nSK!a@4_V6h8-Xcru*!a3&1i`Umk<<$Lo9c$P z28MUQbtp9TNUJ{Djv?xsBKM1DhIYexTX2fpBAyxYqUM<+r;ss4o~F8@t0G9Cn^xf6Be45nz`$Z@kuL+Hc-Zk0bV+^1F{*THp()erR1!aaXhNkND>Z+V1LzE zT4qtDf~%plgaG{(+;yHq5m7ZuxX*i@b1guH{%8!>P;M_+?I7Z`*%Pm;9Jg9< zd-q}DDkVPcqD}b-X`50-QZ5{vBpGg=865XAdPBc3q}CU5qBK6LY?(a#?PbKxxxgmp z4Ks_O-`v|-gV|U;F+mt5uVHpR?tiGJ>T03BoO!8uoy~g>w~nY*kSl)32E;EZ!p7c? zhkd&~$S#sr0ndWg@zhz;s>oMKQKgEx`ae48+by@`V2r+#R2xzA!D51$i)y!Tu#-OSL&3Gb-tXXvoP#&o%}HQKnPW`c7n|UK43LaNcmLArWR7I%e!fn<7t+Eq4gD?I)HiX`6@y( z$Bc(p`if{F1o}}xBy}OzU_3tM#m9*j7o}Byzer*<$LI6|H`;9If43i5na7cBcQDF-Kobht(5wWlK^}x!AcAqtxwr+Aq3=Z(<&Vf44;AsVyaejiO103uxtN0aqxg=Y}=Y~gyIE?0}B zHoc6ncq0ynhR3MQooDo3xfX{CiJvhDC>mL+lG|sejS!oE_gA0En0{37ut^*VAqGOa zq*?~;?wGJmYy<1=$1rPk=?>NR^k(ZezBRq}WY+Yw$Uh-HX@ec|7G2W%yH1ve0Rp*Ou5(qT0q*iH}p&6e8M3OD+{l!IutwStkBb~8AE8$eVhJa(V=s=cab91OeJ)0AGkxdFrAdGE1H+MNP^ zI&U+t^~bgRH$=0|6YzLc@#$x^Z?|Q~q<4$p6Eg9CLufRa_-+fGO@2Re#)&@dkc&`G zu0L%yeVlzXi)e3Wfo=IdgH zf1uty?6mmh>~$&}nBm@aiG^?8WcS+9LDa9Abk$niqbJ&rDRF_Al58%{@9?v^^;t1X z({G=DYx8XHgaaQ&hr;`GplxSx>ugThC_0UT{WPjM8$pan{^`thTevADlN5)oc2_{& zu*1Yy$dK92M5L=0M&8C;0?y|zh|dI^#eM4Pab+EOg&rN%k!@gxr;bHY)!o@`FXsmT zZ-XPXu?@|qy)DKO+=TlA7>9>QHbzV*o2@N>cBGcks2jcb>N{sC8fau=jjQ{1i_>JI zQr@Z|&67p42_x#vgf>-lHc)alS8zHhHyst578Vj>A4fD1+toU@u5@gTb!=bdcmj=M zD+-6BZ~U>#aZ`O@BVC}c9?($-=qCSr$^J(!+U?5xt&9AvE?d9A-_F9-&f5H;3-%Fz zI>SGK%Af)qYO>wh22`@>ywlShcN*B9vi5p7D)m+mcLLb$w`{fH;|Zqiyvx%aq_YRp zun8Ar>E2^!&H9d!a8m6z?fd=oa%0~Aez)G7t~uVSo`*oSjvER!qF&9_T##LjXm=TR z7+d*xXR%8~JpyFIhT0}mK3?@T*n&rYe>Ka7rd~c8C*WwOy%D1UzGII2uCa({tMB3Pw%`wHK7?g|m13T< zWz3V--cd{<*VpTWWPYT~F}$E0!nf>0^i}yGs?v?#%0?;ha!UtdTawQu{Hrfar-F%r7lLi~{*`eFKOGP!(bNyo^HkQecg# zR|sin=+#~~ybXvB?#9epQlvW`>Ml{QO>#&dG@i%D6L(d&5ja}! zjIIZz7V4^rqhI)GxV^634m8*^0<#HJd;Dc565a$Mhs@h&0YqGAmF_oAQP|_St?q5B zi+VRFU@DZzyc$Tik1v&fFC_#@EU{y+ygl1&70E~W+PJcz`P;M@_eKak<9OxMx@{~U zYv2%DehiP>&l2Sp{@4!L$#vLcQsm&*mGrwUfOZ*XT`IXmw8g%q-pMsJ5N zuEO?txM13tJcp`(IX;{%h|Oa_Tzt93RtF+zn6@8umXB+^IoNgCg$!a>+CG-9(1LcZ zi(hNVM4)1?$Q1eX*zydu+of((l!|=X_7}ZQBi|@=Cl`D=kW9NfW7ql>viVzMY~<5B z)LVwl6F>Y_Kas;q4{q(wqo8H@HWq#jNx@Ey@n=5eRPSar+&=-8uk}yq6Q5VeR>~yYA7P^K6aW1>`cgsy#*Eb1VD@Kpa{^mv4_ML2= z(d{vr+#CTJRHFS@8O5xj+*ZQ34HLZ?0G*RprJWwsD88M9a2XBQ`zbZ^6cCaOk8xpTQUBa~u60Nxj)&|>4 zux6`OA*}HUOfik3x0*;*?YYT1ybULqL)ZLBR-AnxrmS@bN`E$u)_Trz5_&dFn6UNS zYTHe5vYq|@EhjhGc4m{u@8@^Uq1HKLs95Y^IKl*ciVHFNe?Hmvf|Var&htX?rk#1i zrYYQ>oS!_WY4JXfx!;d!wPxfc$k82eLf&g3)cPQ zfpa8sXD7_jg3i88yi}VULFvTWHqm){`h_;rTw1l34jM~%F?^Opw@HnH zG_y}e=bMP{-lDsY*zO>*+kF(B{S%w7ge{5m?TPd4f2^A=i1F=-@J(G1J=_m1*8LYH zUz#(?wLyOtEn8Y5kGFk4vs4Kcd=@I&(`*Aah~{TO@nr~#StxS%kFcZK((!pR5Eh+6 z>uYo zA{nZmf3roJWYw4*9es~s^!0`36lI~{*$Vxmhn34}jt_zlUKJ zs3uhz)dN^0)w;-RSiF(04aghmjRtm_pwE*V7!DZ7B@FmfURlm>3FiYg81T@TBUy@) zC(^~{FU08rm`S{knTqYL;v!yZH$Lz2rcGcnf4kp{SM-wzLjCgC!zv1i3Lxp=MVpZOC;1FlI*w^=num`6udRvC2#lA zbW0XIHfbrdo=4>5l>EogImF9Tc~+Rqtw6MXqgW~*$_dn-Qc+B`IU3m*;8RbqWaQ;g z@_3!KyEC24zELF@wo)wXaA_}#GiFtGf1lcd`738ddYx!koQS_p=H>pShRW4Q8yOIS zSpOKi-+Z*8pA4r_!Q0b~6bE8tJcA)rVmD!fAb4J}f&Dj(jf=+T7y*Y+)QFHq5EzTM zP=KGp5DEk1rMEg30)fvk3g#+lQ))1#@7I-5%Cx2Ca+9L9P7vJ*KPg|0Me)2+G)S44z7 ziHyMpZJN&jP#YEOc1Vg_Wz@vgr4L7Vdmdedbqz6P9pOxhWVMKA$#4(>e*)1U3IeN3 zlASlk;%tC_4#u5ok+m(V(cezJo~E{5Rq(ohlz9dgLp(2e`4+q2v#^euWi%hI^m(&= z7GiTcbDbyn0Cr^#&A7tsUS8F!Lie788{NV*H)AE;#+>$%CBZ^*fu z;4V`nL$7lN;}gprUfP5z&+;sce@<%0|l23tmQV z=-1!zM|9OrAz%+wb}Smg@z*wcVf4La958b;1#~E~B5J5Kf^zm5%o1D1(j#kVlKf2@^u0E>tHM^{SyHsP6<9{EFN+aZ$*Ym<^*jSupbk;3Mwy?b2L zh&cHsdzgo3xWa)4dD}*?A#5Z)882&~edUko9TBK=j4kGF8Mq<{$Sk7_tE1#5%~G0j zFgjf+@mnAYD^d_h?btJHWV<8+Plg&5#ava6yn<*mKL_<@ax{rB`x9h#(Pm>HR+@0TT7qQt7oyQFPcMj zW~WY_Igfm(e_wyPOBadyNPFidRTDIsf~~zlS7oNJml4e`YD@ITc@ zx*99q@Ol(2Gv+~D)@nYahMdzPN!7BTWtJ*iQPv zm^`9Z=}+eS-nd6T5Z`0)m&dz2IOLZV4EEUIr`4P&>-vGc<%yY&j!w@9?(W2Z!z`ha z>np~`@1>@y2W0nCbMMcO&n`URRoOGq{WI$Qd4gDmK~oiEmrzR2W7RJvd4Td3Am{|v zQsLq)f0T!jnx36a;YUkxvs2+Q)Su=~E6@}V0sx8lTB~@ro%F}Oc2rgLe(``aLFhYD zfz3R*5H#CUzViDy1p`w7b@;BkOcbs1GNE&G8ZP1JON208CS@7l;wqMfjlB>`aJWck zmB@OPQ)WuRmj-A+8DN_1^TRk}S`MgrR?3Lqf9qrxW2VadhRxS2Xe~B(L>ogdWI)!D ztZ9!$z3D?CVX8+sY-eY?t%c%Uq3~XD4zX ze@hss;!7Px{4Ju(W!k#%xrCk*Yae$tvj~^0eV0@d!Ng zgFZ}=x`n(%x&`HnaxhC$BzL65<8czGDH5TxbkZaGJ}Z;wZF-nIkCgsbV)G60W-51p zV$mks3e54Kk&SsK$4l;yj+`Pzj6U%UFWV1)t%50ZqIf0P;mZK{pa;cX$HlgE0)~mDj1x-Hp~@@L(vcVo`(`B=+qvw=ugq;@|!CUeLK}9Jr zKDs!gVxK`$sjMxae+drL75!_i@iSh8FKf^`KqQG}tz8gy@}7xAE9#!GyJWE{amoOf z^^C{M$mEnfNldL#8w($C=%E?I%9xZ%TmWA!1|R0Yo4@^i68INJMs{+9a^{0G^7ruEHwCe<$=ra+$jo1;s~I%AV$U9v?GeEe&&)ggzMe+`rpOx&jh z0o4Xx&`3Ao!Ba&LXBDr}cug19cx4Vr^JrM3D)x9}`UgzgB0)67N=sD5q8Zv|w8(KJ zhCPa!hTT8nHVpIk*T%r>W~AFizED9OG$fV=t#$L$DDC$SkXuD+wdqQuWbM|Wrw{U0 zm~w?iWLq1Sf9vZEzAoeP<5Sc$3>A zU$2HQpPF2ZfJjevG$)u=bt7dvM<|0FyKnw@ANe;I^JI9mvN(QHTY$~mcJ ze}Q~zv2I=aDMh)JF_~GneY&Di-g63+=gU7JFKy@`$IYgj1+{r<@GTy75h;EZ0 zGr|k(?9xvowRk66K=>zN3c9h+_NUq;6+ z8du8U_j$IBaH`AoYLypN|A`hAzx-)F=ZHRLk|T-DH+#D3zuA0Cd_L1`OL()95COVmuK8OwLZc&NAJ{)B{+^q;v2=-2$E5|Fe-4o! zqK1#ZKsW~VOc2bENfm>1h$Ac9KySq$Xn&c>DqjseRIP;~6|gPvA`Z*iwBzoqz#b7S_16lL6Pl8 zF<=sZD)(S;_vAqw;fh1Qi+U&e%U`0tc(8!QS~{MXA31*eI0076~UKs!Y delta 10908 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 z yzfem70v-bt000080Iy~`R3sFINen6k008R{ljSNW1DcBQlMgE(22?5l0000XyAgl@ diff --git a/version.tf b/version.tf index 4e8c3aa..8eb9976 100644 --- a/version.tf +++ b/version.tf @@ -1,3 +1,3 @@ locals { - _module_version = "0.0.13" + _module_version = "0.0.14" } From 3f8628b7507ddce9cedc7292c0f5da04e48530c0 Mon Sep 17 00:00:00 2001 From: badra001 Date: Thu, 27 Jan 2022 15:53:05 -0500 Subject: [PATCH 32/33] add DNS_RR_TimeToLive variable --- README.md | 2 +- code/ddns-lambda.py | 14 +++++++------- code/ddns-lambda.zip | Bin 11061 -> 11100 bytes variables.tf | 11 ++++++----- version.tf | 2 +- 5 files changed, 15 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 815aa0c..7b3f2af 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ No modules. | [component\_tags](#input\_component\_tags) | Additional tags for Components (s3, kms, ddb) | `map(map(string))` |
{
"ddb": {},
"kms": {},
"s3": {}
}
| no | | [create](#input\_create) | Flag to indicate whether to create the resources or not (default: true) | `bool` | `true` | no | | [dynamodb\_table\_name](#input\_dynamodb\_table\_name) | Different DynamoDB table name to override default of var.name) | `string` | `null` | no | -| [lambda\_environment\_variables](#input\_lambda\_environment\_variables) | Map of lambda environment variables and values | `map(string)` |
{
"DynamoDBName": null,
"SleepTime": 60,
"TagKeyCname": "boc:dns:cname",
"TagKeyHostName": "TBD",
"TagKeyZone": "boc:dns:zone"
}
| no | +| [lambda\_environment\_variables](#input\_lambda\_environment\_variables) | Map of lambda environment variables and values | `map(string)` |
{
"DNS_RR_TimeToLive": 60,
"DynamoDBName": null,
"SleepTime": 60,
"TagKeyCname": "boc:dns:cname",
"TagKeyHostName": "TBD",
"TagKeyZone": "boc:dns:zone"
}
| no | | [lambda\_name](#input\_lambda\_name) | Different Lambda name to override default of var.name) | `string` | `null` | no | | [name](#input\_name) | Name to use within all the created resources (default: inf-dynamic-route53) | `string` | `"inf-dynamic-route53"` | no | | [override\_prefixes](#input\_override\_prefixes) | Override built-in prefixes by component. This should be used primarily for common infrastructure things | `map(string)` | `{}` | no | diff --git a/code/ddns-lambda.py b/code/ddns-lambda.py index a70cc45..491cc82 100755 --- a/code/ddns-lambda.py +++ b/code/ddns-lambda.py @@ -39,19 +39,19 @@ LOGGER = logging.getLogger() ACCOUNT = None REGION = None -VERSION = '0.0.3' +VERSION = '0.0.4' # 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'] -DNS_RR_TTL = int(os.environ['DNS_RR_TimeToLive']) +SLEEPTIME = int(os.environ.get('SleepTime'],'60')) +DDBNAME = os.environ.get('DynamoDBName','inf-dynamic-route53') +TAGKEY_CNAME = os.environ.get('TagKeyCname','boc:dns:cname') +TAGKEY_ZONE = os.environ.get('TagKeyZone','boc:dns:zone') +TAGKEY_HOSTNAME = os.environ.get('TagKeyHostName','') +DNS_RR_TTL = int(os.environ.get('DNS_RR_TimeToLive','60')) DNS_RR_TTL = 60 if DNS_RR_TTL==0 else DNS_RR_TTL print('Loading function v%s: %s'.format(VERSION,datetime.datetime.now().time().isoformat())) diff --git a/code/ddns-lambda.zip b/code/ddns-lambda.zip index 09d2410dd578c9d7389e524c8326d516c8ac8535..107e128a71ad3c01bba8761011737aec504f4deb 100644 GIT binary patch delta 10553 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

S&EK*S8XXz?!2U_`_uR~l zrAw4LE-m16i2M*WeEbE%F{o#PV17)h7^FiSS>XnHD+WRP%S=}JYUl!W=+s~PGZUt8 z_{`FIF}+^ovyX}<#TPGC(zjW~uo|OkEflGMZGV9maah)-9d~C1_UmNh#uwivox?#m zSZVYgLU+H%-afgs;7TZ^IVta^RX#1XE!%iD%h&ja)IP=W8jt$dW=kRis&N`@~0)Z|%s5<%6qJ)|P9+8nX(MKfR@}i$w;F@ zomY3#5@0V6ifliM0h9Psxd(&0ClBHXR~-6X)H~5%{u1@Yg9R+s((%On$no3Paed^e zcQGLD!J$wj-p>C4P)h*<9s?8r000O8jea{+)$-y#sww~g>67FtC<6)x^OFxNAO`X( H000002X1-L delta 10496 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 CXI2XU diff --git a/variables.tf b/variables.tf index ee06181..fe23c08 100644 --- a/variables.tf +++ b/variables.tf @@ -20,10 +20,11 @@ variable "lambda_environment_variables" { description = "Map of lambda environment variables and values" type = map(string) default = { - SleepTime = 60 - DynamoDBName = null - TagKeyCname = "boc:dns:cname" - TagKeyZone = "boc:dns:zone" - TagKeyHostName = "TBD" + SleepTime = 60 + DynamoDBName = null + TagKeyCname = "boc:dns:cname" + TagKeyZone = "boc:dns:zone" + TagKeyHostName = "TBD" + DNS_RR_TimeToLive = 60 } } diff --git a/version.tf b/version.tf index 8eb9976..382b328 100644 --- a/version.tf +++ b/version.tf @@ -1,3 +1,3 @@ locals { - _module_version = "0.0.14" + _module_version = "0.0.15" } From 5bdcad633a07866341161839b39bc82e494236e0 Mon Sep 17 00:00:00 2001 From: badra001 Date: Thu, 27 Jan 2022 16:16:06 -0500 Subject: [PATCH 33/33] fix code --- code/ddns-lambda.py | 4 ++-- code/ddns-lambda.zip | Bin 11100 -> 11095 bytes version.tf | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/code/ddns-lambda.py b/code/ddns-lambda.py index 491cc82..6b23b48 100755 --- a/code/ddns-lambda.py +++ b/code/ddns-lambda.py @@ -39,14 +39,14 @@ LOGGER = logging.getLogger() ACCOUNT = None REGION = None -VERSION = '0.0.4' +VERSION = '0.0.5' # Adjust the logging level [logging.INFO, logging.DEBUG, logging.WARNING, etc] LOGGER.setLevel(logging.DEBUG) # SNS_CLIENT = None # Read Env variables -SLEEPTIME = int(os.environ.get('SleepTime'],'60')) +SLEEPTIME = int(os.environ.get('SleepTime','60')) DDBNAME = os.environ.get('DynamoDBName','inf-dynamic-route53') TAGKEY_CNAME = os.environ.get('TagKeyCname','boc:dns:cname') TAGKEY_ZONE = os.environ.get('TagKeyZone','boc:dns:zone') diff --git a/code/ddns-lambda.zip b/code/ddns-lambda.zip index 107e128a71ad3c01bba8761011737aec504f4deb..c129dd82cd9142c951d77d3d6b32a6b4e83de055 100644 GIT binary patch delta 10533 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

  • G7Xp z)}Ln`rXx;Yo(?nnXH`CWJE#oT)H+LkUD5SwSRpSQ4^z;s&j-Y5!MdM3aE?Uo?1VX5 z(Al?%muizED4kf_COS_~ztCoyORLt>L1PJjFNV*O==Q0Vlr6e)o>oKYo@VyR=zJ6L z-CK0`5!)R^cDs+Fvwvdqm9Qm|zCCfioprMXF}^(!zNrhMhx?($y8oi&OLHcht@|K<$`5^Nx?k|n0 zS{IoOi#O7>0eK_6(ZEg<^m%dv!vO<-xr70q$}7wHE#Z8?1_K@%b0kYq@Qv5cPh4}OND zEAi_k<#4=G`QP{V&8&~&y-DY?FR!$<&Y>JbX?W6vhe%Tqir;{(WZ1QMzo)?Zp-MO>{YlLaxGs2f}Z$G>>tl9cEIR@4Bl zOzAKqz8o-ylcPqw0SU~v9`N8norO?liG&(Nk{#Cq{bBfrg14r-mOtHn~yg1 zli@Tfcze2$;y{dyXE20H>?Ujw1kWoru>Xd!anbl3Bj6B<8WGY60%P%i77FlF7(!uS zy!2MbLLl%NM!{SqZAy|{H}tD%`EavW=p{uw9IqG_7e)w<$ynr7W+RQnNE z!0NAI{oC^J{kl?0+16)&*Lq(g(RKE%3wrDNntJHzVwb}{>x%4;&0q$|Q_fO|Ho3ru zUn7%1PoT~Q6zVz(RE-0LxT+cIm__L7*B?yrB<>&0#Zd13k?d=i{g8n9tl=OH;o6A7 zPy3RDWVg4?-nb|Ock(3M7Qx>Xl{;fjcmCy_DOpiT1` z0BWOx-401{tBjhsy7b`)Z_lHvu&yDdtRtLBk*pT+EEx_WKp+}KL11-Bvh&7RoDJ~L z!MIZ`vbIGv`rE13)6~|h3SRe*GS9$bi036Q-(nYh7S>U-jON3YK5w?qLTpZFuJZ&R zz^=@p8CRIy%d2{SRp{Q6aHCt8=4Py<+nCcnvLsk2jy!CJv30A_l+E347u;ovWZXO$ zI>oqIm-rG5GqashqQ};!cv??#U|Yr10B-osT&=YpMiQB#F0Z5atakmgsu_sQiMSd zI;b{LQ#J^gMnZTD8CXQnLIBS6dwOBbzdie&8c!^KP;$a;Vp zEoKXc*_AGTYW$EKe*0Y?z;eByx3>EeS}1jAu}(Om)@AfF6v8RAXFNKBZoT8_Wxz7= ztN0dXm$lLkVDYg3=t`;ICOq@fBY((jJ7iK}ZBo*!@j<>aQrJATcaMu25hved5A*O0 zS2*w>Z`%kqgpH&p<7ExBulzB+BLa1fvBlgi16Krp0hwi#VRe+;q*+Q+4o0UdC4LJ; zVMPi8sU3TUjcm8(R=RjtQKj?9Q}lPc{+0a#%=GiwU9yZn1J2Nv#P-d>`J2rUeqZGG zX$hm8%rEYWe0_VjT37o%M5D7Lo*%}mc$QWVKf%|MB-y=g> zdso?iCIZ~Qn?iiM!_vaG9Dd!~xuhlB-gqxctR{W*d~4}*di5+;^+j{2&g|5wGv|>H z_3KY}=^{}dY46;mYJw(Hu(dbns?7BDGNSoKZHfLkui|PAqLy;#92{Pp{CphwROcG& zSJqc9B|W)GQ@jarktD0(tM7cOHN_%)9$k8Wb{A*2P6!~#Vl{Lst4e*UL04nN8(xp1 zWyU;+%UaEc)R1#pB&k{!w9HavE9&|hchqYtYVQ5{@!5q3yefMpx_?H!KTi=H`pd93=yBo9#D0tB7FS}I(;h4L^` z)3dWF{Aej|b}BrE`qSKL1)AbP03Z=xYZcG7lm58Zj;e~@FCK6v2z^H?u$d`68 z_|gCkC<9EBeSR2cOv?c^&q^8bd!5W;%v71*u=#ogt;Ob!Xk+Mw49Gf?HSMveH+?81 zO!Wwd?d%K}=$F)uLk+uzjsyZ{9~;LavG~b?K*7cug=iX<9T>l5kw}R|Z`K)q_bl@^ z%V3&P-Tl3a%aRKQ3b~6H3mj7kYVvtRs@A_=qPANGj6H=WW|^ze^Xx| zzeRMpOj{Q|m(X)!?c=V7HUSG$fxCS1>WZSUCssPfT0rg_!V(Gwn6pux>k2rGUzO^V z!B-{xsB+a^1Mk2YRkycJ2UYohU3QUUq~|>l7^8&I6_~SCg~X@|>B_F*Myyq=pbBbW zCKjLZ&O3o!duDNmfO=ud?teK+iJn8?Ldf-37|&iIkAX&0o;Exr9)V|m(1$5fw~&`e zx1fAc4rWP;+Tc;o=e!#Tr zouvz(KN2uJU%M~>3_;lwQ7L6$n$Z;$Nk8e=XCDA2RMiQJ22}$1 ze*L#88mv%&;6akml2SvUO|@}4ye$OVh-0W>7|{`4Fk1A2e1-E;A70)te8Evi`HO}) z&>PWcFoprW@;cSIyU`j99t?$5EUK^{Bk(~W*)U6`HAA6bb|>S1kyuL8MGi_3$Ev)1 ziLtKsPP>ORj4K;-Na67mrMM?-Qi6(H4V~I!7X2w2U4C=r;Up45?WVE9JhY-ua(>Na zNohGH?yC+l4&@)sZWg?*rS#lK!_;9=yeqg!cwZIYp`k~H&QJZ}VMMx(fDsa^vcZJh4lqy!wck$e;La0wvF=8W)N59@gZ$ z5@`2ZpY*|h(j(1`a+mx}vm@VK{CkDMn*<{Tt`;*!2dS?_a-Aj-98{D79(ZALjKjTICvIeaKM3Pw6+67@J@0m!nqV5U1OBSmVrwnje&v?9yOisy@ z#MBzKvG5Ux9-1+%j7gcq1@P5k@L>+T`P<(ofq!9tWMn5tC}%!6Q{M&JhsopMY+B!} zXHxz0VG88Qzd5>;tTQ%A)+Jj6!N{pz5$zG#6IP;KA^jdT+p zJXHj7R`D8**K}cxSLTp3kA^j>Vvk3rf55aY5=29+v_w@bnxSn*iyTK{*rTXv*!?4J z!!Un;e{BrBZbrIYzlK|^9`&{{V?jnaPa0J&A9R-3LgO4e>2dio%5g(+8PM7FhI zxxUWe>oP7sjYAHWSC&S(OoJu#Es=xu)2Vtr=(GCE7jq_~fDYeEL7%irQ;g&pH< zH*iQv&d!pXq)4DPB}-VZ)AI%_3FZ)>x8%*Fg1#Po>5a}e+ zSvAD}0olgIgJb1j`I1ou_Az-7``&d0!x0e}&bINap^XW$HIibG2-D%mai|l2_ap8< zf)e*+Lo28gI<`9;c?=2-p?h)`hJklLUb7P|b1%r$?@Mrd?o>;wBJ#ou!?HbSIk(;@Oh)bQ~a2*;qF z34-}CsbY{0ab$%X=&cw8?JqM~<*T6!)S**)~n|9ot71*znjT>Kln{*Bb;b5iFdkEeA9(()b(t<0Yl;)(o zn^yU>)V6Hn*(_h@6aWAK2mp008L|0RRpF8~|iwZgVYcVQpe$ zVJ>iaRa6N80}2N7VGstfH%pfiJvoIJNj19?xJ_U(-y)2mQE7y|Iz68rzCAgvQG}Og$WD&4dv-57$#%}) z9v{EIH~|9Nhk>nzc{xe4`?Sb2?C&r-UnI%u0!BZ&8b^PA8I4B3QNAaCiri;*;iCtj zh#WoxT}0z3&2C=I@l`r|QRM3?`R?0jv~zLr`ftboJv|J-xQK86mOLD09K>}#+nZC*)ftM89R?4sX8iXo zsn$itnc!?)6tHBdHp|w_YY;QQ6!UcTAS|+KtbWe&tV({>URVRL?;RAq{&EG>v~SORT|!XXzK_ax ztl>SpOJ*NYy(p6eRIvnD4-5g5LIpG!5LqlAUzoVb@8LNyUy}#fu#HF=~#;4^zOZs)`g8 z&IA#lmf*iE0Wf7;JOF@MjLJE5B4ZEvnpH`$Ofy6e^ex#WfljMZgGLH0@MCNbJAWUi z3+%#}m85llJ#Yk}BiEpyC#a{+87Vx-r>VER_j{`t*eIXxJ^hWCa^syf!&Z5kV&k2d zNaJ12koUrCLZ~gE7v#eM#|=q$Ok#k3k3c3!AvJRANbKuc;*DF+(t&FHcIfjgUckzj zf}OMi-7tX?msg@5(IE<880KaC5zLEqfreI65GlofH4otAg{sLM1tQJ?s#j8n5*uM3kXr z$;Z)u9!9MpNdmyJm9i_|As5G)f<*}HQ7xWhBtIPJBwkWU{(YaXq04BVRInJMtILQ! zL|6Kh&6Df(?QkGhHm|GA9sM?G)WSwpO~DQ=l3N%&Za1UdHUm>Up#kNSFUn5Sxeq`2 z`9(T!#xk92C~81Gi>bDEM04?Mmanr)G{6ymuD|l%;^VHej`(X2O8kR{4S(Ssv3J?x z0SFJttjw1Q`3G=4)N=}pv{+{u8kJ(Lh`9uVOnitO`WBd!XzvgSgZ%yNcaTNEVdf?L zIgskNGMUB-H-VgJZLoR(US17wX@VI%0TNx~T2^HQDI*IK>>FhZl#UrzMC^^Sq}vmJ zX2VzYA~Kh$0dtTcs*AU-@?SjV7A2d0+gThk&Mp7%O6GY2**@5VRl}=oviZJ(5>h}CR;BSz#bNBU}6p$ zAlUyKB=;=NkiV5Ec7gM!v+7U(b^h*uR0B58XKQ3zh?!!KfO}ezu#EvPy`=!%sXn^>PIj(O)yczsq6mtONZqUN5SB{5UNWV4e#P z1WjiVa28;;)^MnaQA=kvC_YY9fAtT62^17=A+&)Iy;O`dWG)Ex47ex}Vy!=aD$wLK zt&%0Ih+1K87E;Di8T&>y)YCaSEulpcwb&|l%E;a$hxZiOeY}?rG?MKvTPX2}1isa#Hd80_q6)$qhpX9O$baNh*EfhT?cz zdixF^rq#m=CuE**69qLZ{*qS{Dr{>xz**4M42;fOZfprHFDBKOOc>hGz~O#XR-7MF z19vef=j5)RqF)}Nl`-yx=5PA5P*{A*R=;fqQ98zy(A@D*xdafz5lixqlh9MoZGHB8maJp z;N?@OpbuZX8u>=Z5dIo}_blf}$S!MEtQo;cR(0LV>5YaV%(uCc{JMdyY1S1y_}Xi; zi{R-A5|+x-KsA_1?bzL^JI0v_caAulmO>o*$a-b z3D~2oBzM0F6j!l?YBw|~R|{B0(Ii5)=UYjl21{4k%+&+XwkYfdJwW8AuAo$ZJh0R^ zuaJ;7ID0|%d6r1~e6_w_q%#JXh?4vTpI~L#tJm5z@%^a2;L#j^cuy~DO3+t)he_WP zpqz11HZzJM*)()S?4+}g$wM^KDl73`JNzHP5^EsBXdWA|D3zptX!|J4VVt z+<=Ao(9>zG-)grg=c;k~R5SW=k?&e2utt$26mM8^84}c+6Rm|U*Aou&^@7J&+@#Q= z(IB}oBt0t=Z^k`;?db5nU(JZ-VVx=EresNmn)rOrRU%A0NS&}h)Ru->*kuG8%%I7x zn@G=U&Yj6H4|V9x5C>*$OB!^bDlad#J{gFJouTjJGC7vv494D&(;z1d?>>=mwiktK z1C{G*;J<7cmmhI!L^q4LN0h#EuVb&NJEw{#es`pTs{2cS zvi1Ag7gdAtv|Cruw-eM$w`Bp1hKC0O)TJdA{0XSw#j7h{ds=n3r})-wu}JQdqDbhR1|I%WzHtb^OH0H3>xJgq zZACkQ%0c4o zk%ou40)Kvg$Wp%F2uyi{Rrd30MoUb#^xJ3@p;Mva4j;LN0V&&dmRo|Za)+zGj9<+dnqlbJO{sN@-LTe9!I zl@zYW00J07k)vmuyY<+VeCReBnbI8g?Tm{A>WqqKSorMA8a+luL(u2g6Iy_vyF(G` zc$Y}U1L@8KUE9_wC!#W}F2D(Q!L^Slj0hdnoFu-y1_w9gU&S7){%$Dts=aOVpjf>Y!c@yw7HHP0M5g^VfkG}RSteU1hm4O(~ig!uo_Jm5xYdH&yAKmrDe-9+ZOTVT z+mtGja^c`4$w>Ch;JBC38~TM2x4w`QrSVZ^%jDs2FC%Ww1vWWvm{|<{=HAX4%*N_} zi3!3ec@4AkaYr>(R}1y!%uB`VY~Fjgbwss-T=7FTAbv>^HuiQr?A!H0c9FCScowvd zr_PdAMZQXkDpkzY|ItC;Zn32UeyAPX)y)6`9R(^7$~TbuGF}aNBqjTGkNpZ7_WMKB zu*WUqK?TtVZ&IbPVtsVKZdL!Z-n4dQWC1sJf%z+ZRA;Du+XAV> z!TgQ1dWX!$%~fVNgud=zdhmfd&G{)Q94L>#9qR{@=qML@C$H=M@<4>ZZqHVK%A7Xj zu>m1w`J@Ga%jiL`F~X&s-bkrcZHXb{Ln~qvJ=0jvaqWclyCCFjk1W{RkQ74W_$5v; zfPtUFV(}8=_{iUf(P4EsbIewGUc6YwV1VICj2HRMO}!Np!yr{=05w`|i+G(;P%KP`I(lY^rP>^i8I^QGG~{HWXB&fp@-(aD z;k5%noC@YlICpV0dBYT$f*a{tAcQJeJHciFAFvuIqn#se@U2*21$n-ziy7*=2z)@|mZ6$hfYvb(l>ji4ff~Ys5K$D| zvt5cv9}&1rMuoH@%&_m6I~Zl4359HER)W?b4?-;v!MNsJ+ycqa_o5Cd9FR|>W|G4H zK^?(!nmn{4q=W9lV+Ejpz^|YNt)H3WkonQIza1sHdtXC^M-neHA z*Yk9_S|qjUWrW2WaX2(QMs4mqqxZ_SI8;ddj6p!r$WoQuK09rM*u1~`Ovd!1f`?7w zNC+_y(k0b0Xm`hiZDJc(cRz+%t4nvNzNa@^xACp%wI{Qtr$zpM3F%22?2yOEI|XK^ zMZkvN=5h$CaTf6LAtaCKQ1s{z4wdf;!i)V5ae88mm)=jHCw z&EEEApz#g+-fnyL7=w@<>H~H2Kl4nBLaMi-zh|qB7yJ`@C1}RXyWi*v+1%^y1A87zWRK zk6qI46!6n|n|ZB2uI0ZWnr)td$D@i*KdXJaEjuQ?TLhnwi626v$;5YC=xp-)kuy&8 zX@^{davEob51NKDrorTYW|U}V5wGZ+RJp$92JGZR2I?e#ok4C#K?@x6J*}hMYmMQf zZ1XZ-7c=|=_3mM(#V=>CQ{li2_pVDUeDfx|*NzUNe$Aw-*5V#L(SA&c3&fOUb8&u$ zpUth$idmX|`&^r6dnX+DFgg_8rvq&}gIi~F%0|&?6zr!_&DjWIMDkB(uG_**DVd}= zY_+=r@`fFMCdNXB%yuRsU9~XsHs%s=K7T=cCg3dYQ&*2G>&Pqg=%|it1CvK}EQ+e` z&Te}-H~4=W9I1_MXg=+2F^=FS+!w$&JVdfFVmjGuZLuS@j7Ht)#aG`sOVL0h8*5zM zw_BVh8H{0;0)6#>jyga$`QJ}o{2%eceX%Evp4T`KAkAR9K+HktDAs;|KoJo>9yHZ=9}*_z|56MwbNJvQsGogj7- zhng?q7-uKM>gOEFq_ zGM3WYDt=}u9&pbrr9Vpv`_+QM$C?_d4sALCM?39}7!B|pbKG~0MMPVD#fP^Ae^B!w zEUOgrlr3YPwDyi-61l!!CnWPDWsczme1J0Ua2r##82G%tXCFOys zJZ`VTWY1u20HZ81=bm#){INS@CN8 zMVq94omBk301+H5!*XX{Ck^L>dSZ=#v?-R_kEsPjPvl^JX$fK!$d~IIKs7}6XbUn%5|<*~@lbb(dTo+J`k?WDJU*Vd ztGbQA(RyceJt(zMS4|xK!cW8Pb?tVb!JZMAO`zK2FFTR&CI~rX-aZQ;;zFx*zj2Df z9>;BUZ(Ci|yEy?P zfrE|BcLxy>6?csB!KZih4n7?sc<^cSVcIM1(Q!QD=^$tx1Acq4Jr2HGiVSq%`dQ@T z24NeqLmHxe_VeVk@xGpXHg#MWNVG=mI6d6S5Jvu?_dOlx`arj*1f6k2dlG3=hj_sCXhN+u+n{lJM+rfanCQL zkc~8YJA82!w$H-_)5hdERL$|>Y(Z=u1LES#Ew(xkLBq8DptF2jwrULb z3Yu(RIgf+C#eI1fBe9h|dmUFhL4GHCd&E}0d>$8nC8Q8(i58Mi52mnPT@Ue-KmL$fSK{ijRl<%oLkY>dF*%IEa%{n4&Q|dUN=m<-E-@ss1WT;AI2W zCk#)yNpIIhog&k0_CvO-KOZu~BDJ8Q4f!E+&+HovH*|R?m?~_3H(Tl0g|mHO$tf>c z)~1g3c3RQ}AX8X>7?>-qfVX+>WDKuU=`}Pd>Z40UCGVD=m@$(n zwRxzOh!o*_(o9JfO86&CGt1#!$_phyL=^d|z#IVT0s>Kg*%N&+;W;!ZFDaO~c}vQ2 z-b*v-%5_qGN|MaZdx&FO=Nk;fy_lnrMb#KH73x`1)2hxlXiy1VBd|}yejSVphofSB!uheOX`>qy<3{LK(G!r5Kc?( znYBMATUvO})#*SuPu>@iNUb6JR*&3j{#9Wrv4bsP+>m(f=@PDOkZ8?Kur}CUf;C&M z3So^;V2WuJz12jjYR^s9;cYm<9J=O5vf}ImF=efPJ5c(wX|&dJmXpx4VZwy1=T_Tp zij(c^_is75$+k0_JbpjFa}Kr68AHWl2g4C2;8R?P(f7%=7p(l4a-J88H|@+DHcjF7 zT?WSUTOiGnNz)-081{_D;0J2q%<5#E=xo}d+ zXFVx@jF^7%-TG!kXwgL++=C|TCnaX51b>B zJ3C>H7IgM);-%W;2udf`wu#Qu(=W7{=F+NvwRF%}!i(XvB)WZSC1s1QoTt@Lx~G|a zGCJQxeD@aJeZ+PLk=^d2=b7r;#7eauvBZxt8uQoHeak2h@sliB@VyrQ2(5bBr59#&CEQ~*f_C$H^lX)L2B z{)3<4=t}&0NjV&^RQ~tD^ohmh%X0>;pC_hZ$JX`tp_}KP-h{OSt6kZkz~iUKz|tiq2R6QE_u6`rdzV$ zu}Mpr^*kacr{q6|&LLi&%Co|MTy6!T^&7=f`A|-v_LPcZs?E{J#sHssf+Zs_hmyzZ ztlgdIWcH0J!LXHLS%*t|S)4JevisB)%wIVx((6RS;zay)GB5WpHB_!f+Q@(q#QMkB z{pO<${bV?e3f`V>q&N^G;~5O061xc-1i|x)4eY;RY+N)x#|SutqDF*&G=jibyoCb% z6oyb37%#omu@DG+hEXtANt==+*A4w@T0Yz?;D z&NTlnu&q(=kR|G!QYJvzkkMwv}l^9b+xYeoTgd# z8P$Ho6|nkiSpT*>e7~-Llv1|!*|px+NOYZj>w@08zNQ{}y4dBg&$=S}V>6fm@|3d_ zqD?OF;n&C{&=aV$0foAb0#)NcA+BnMI%W~N`t=7>Jc;{9b1{^Aez%?jsAA(^)$8hs)E=3qs%j~7~*-!%eUACpM`bQETj2wrO%t~vk;rp znd>~k2e2!1XvP(PX7}={UKP6cB;4p0rnwm_={DxHk1PomiX#u3VQk%MG-Y$Q+XZ)- zA{jRihE6eV7C9gozRicT%Ksvwani%-G{ehDs&Rozn2K!dCWv$zujg}Ur!H+!Pq^_g z`IL?HW7cj&EkHop)r5kCnt<%c17@6toZNU{1QnITjfkdyG)ZNF zdKs`x{3^bM*=4P?16VxlKe|%tw+YX@^vEAF+YXsjSeumeYJ8Bdj1)Fc?cL*|M#RZC z*~2_M!xauZ$lEr84Phhc$#_`hcgw&PK|p31Wmp|0H))pAl!MXf zN{QbBQCN|JKx)UHVI$kExs@&+R#fRc@)Z5uu772}05ko3c9$&U&ww+uC9!>TaQiRXv$DxRg)!%r|Z=ym!JA9m3%1uQqGs8#ZJ z?f1xkkk;N+wuu1u@1_vn?y$75Er(zCb}nfNw>RF4601ocJ>OdToL)VPRejMMsxv!v z>dblML;d>GUAjorN7_3#shXh46m0Dcx+*h$y^LsnQCp%v&a1dugQ%q(ItParCqExY zKGnI#`jz#SOG!^|(iCq(TqMbA`06{KYE7|!2%krnp54XStrG$WvRDnB%BoVIYS7hK z@rKu1ue5w*^0Wp#vS$D{mA3WI?T7jl`5CBNT*ILE1?W8~MwWF${_lpOd2}0kI3T)=d zg`nA<@|EAuDHxavsKa;NWuj=6mkFJJo6~R!M_(d@=`tzH_!d{OENtwBP=donI;%w1 ztDG`Z3cfTz1Ihr?WS<|#8Pjq=&9hQQ{9Y%s7&BGoH*CIML2I$OBia~xAp^3GWKDZ4 z>P;UC2~$17VLLm+1^Oj*<50t{p(BC7*~iAQNGyJ`AW*QeMj@JpWe3J@StL?_BGH?5 z#y!it%`%v#RCj-`;AM|00 z)Gg#C(k&=ol!IB4BDo_a9*>hiO_2zlrIQ}f_gR@dZ_~r%d8G8e5}R*-h&NNY0~Cuk z*;ZhV2aRmZGdW&ze;hBam+${a6Y=Qy?eWF&vx)exVj@1?LgWlhuHr{a!LDKu#nvgv zi61cSdS~gv=Z^#o&(|&t07FoC?lY({wSwvRK^bki8mQ30oCwqN$7S8R{P%)Y znF5kCTDcpHulCV<*(d~mIdXKBi98O=&-uvT#04-XTvSRKm}YbZMbc0D_1Oo22~~B1 zqCu4azF+@siUunbAb5}@w4~G!Xj5&R4sQ#=HsTm+7)Er27mOCYAYb9U)Q6Wh3}0~6 zQU0PK4)jJe8jN8;ue?rm?ryXOg9k%l6^knD#|V57NH)w;Y0Xf7D45;JcqEq6bdiG+ z#IY(bUt+AQz0>X?4dcoN9a4BaMJetHo0Om;S3{@vm_>hzMwj1Qc{quLP`hcYFb}Qh zlbm03SyEa~iTkQUj6?ZHvzrC4YbibV(J*xw6z>Wy65dzEcWCI5q4QIJco>mxBVdGt zs%)?&GksNGEm@v_Lq5*TnO5;H?3?et^UkMv-+=~< zxFjs9!DQen(3<{CW%r=4Dg>5H{)zU?ie!Q}B&CaFSPX`L{kIpR%Yzqx|1f`X_1mlQ zpTGTm_}Bl0cmGam|K#NL>ASPz!-MlAhKzKBI zS`w82&8Q{&ZYUv1?W>`>3Qz1(DX%^vCh{kKn?T7ks>Veky@z$YQ=8zlZ^!CLyNa{g z;cu>`r1seLrc4{j2tF!3Cm?*cbs2^96A;$yZtAk}U;HKO7!gw-uaKSNn?s|5#@_qf z`_+2qmjv2>{njUau=GeXqueDw)9lE17yn+N@Fu}Xfvd%g(Lw4fkzA)q1P2wR!1(Cm zjEa2*Nu{#3fF?LhSM;y7#?N>WzN|s(0Ffk?wRS<+$$KUet*Cp#?vll-#3=(@)-xV2 zBa>6|Br&x{Z7h7mp@(J+D`Qe7aRGd_7<`xmZ~pdw_etPi7#Z2g5z3hl&eV5-_F?ik zIGfft>zP!)e3$}x@^6kVCF_h$l6A=zLGbaf$ySFPUN%rlG<|s$T)#S~tuI<21ymb& zK_lIS2Tv72oK?I=<27Aa(IUr@81^V? z8g~DGh}$sC-(MR8ubYu>7x_X3bnSgx-#__~bCk7F$xoyARmlAILF$$LkY5-)R;2#c%(I?ss>0 z**U=`EVU`sTI^!&BOrO#7$DI5zC}Q9HngCdhJKWDRfJ>SN4m)G-xE!HrZX> zfVN&HDi7Pl7t~Bo1Vtl@?HHJk#^&6}r_Nn(RWK|_vsHfuoqw8;$i8d~k=OiJcAz|e z#q|anMaHbBs+%|g;y=GX^apUmhclYMXivhiZZ zSz*U`+YKC2lC!hqCMgoAP0140>vTTB|Gk2#vMbbnJ#M@BdnR_FFcjXu3uV($IQabO z!dvl7^%Mp&jTgiG`X82@_^6kiOT1Bmf@Q4v&7 zy61+VfQmg7>a>(ojd+b24j_d}$UiLAlcNIU52Yt*ne&WGj=DlL&&z%m!v+)hO+nkN z9z;5cbXE=Ve?Ycz@!(iFSiWRbfqhIK#J+c3!Ei(bhO=!vYiMJFY>lKCB*Jum_;DQS z#Qli-kD$ao+0Y8=gpTbFM;?PhL+GBIg<;?wkk{-)3;D|+T*BEx*o$U6idW7_EeqsR zi*@VTPbtc+jLFQp?b8*F@}5(mJYW6+d1*rjIc_%HEU3*>gO54iRgVKgLv))2nGs%K zXP15&sl_|lDu=kh*zX`No?bkENO)>50n>y_H*q;lGUWO5<^!d_h|L3xQc_jX!!ByK zI9mH1C~a_~()DBVG@I#1RfpX6BaYNjR=$y_S_zR!o!A$1SlsYag;B<)m5H)=K z1;R0?XM$jUOsW{9LmXM*26`(7LHo;0R{3h^0(I!rU;8r?rf~So(s?nxUgWcniYCPu zFICdFS;ep#qiQV_seo;NffsRD)}|eIX9f1_WaGvc-zJ^IK{!}x^d3TYzsKG_xwPO) zD5W_m@1|8gEwwG%cs9$|_=nU!#qk=C`uUziU@`O~6=ngSk?ku}jrB;E3&pIYFV{{XPe+(?~QchVAIFAs`rKZ*g9 z_*1zDgS#gW;s{q9`d!pJ(O>=&^~Hk)EY{NT#Qezd+t+b@008Ne1}i863I>xfD