diff --git a/CHANGELOG.md b/CHANGELOG.md index 992ab33..e6035f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,3 +26,6 @@ - Added additional "sleep" timer to reduce Route 53 API limit (5 per sec). I also reduced the amount of API's which were duplicate. Also, added random 10 or 20 seconds sleep timer to reduce the probability of API limit. - Route 53 SDK will auto-retry up to 5 times, however, by adding random up to 10 seconds (running) or up to 20 seconds (terminate/stop) will spread out the calls if multiple instances are launched OR terminated/stopped. Testing performed up to 30 instance started/stopped. +* 0.0.21 -- 2022-02-17 + - update code 0.0.12 + - add heritage functions, but not include anywhere to call them diff --git a/code/ddns-lambda.py b/code/ddns-lambda.py index e3ba763..521dac1 100755 --- a/code/ddns-lambda.py +++ b/code/ddns-lambda.py @@ -1,7 +1,7 @@ """ DDNS Lambda Python3 Script (revised) -The script will retrieve the information from the EC2 instance or from the DynamoDB Table entry. +The script will retrieve the information from the EC2 instance or from the DynamoDB Table entry. IF the instance is running, it will create the DynamoDB Item with the instance data. * PrivateIpAddress * PrivateDnsName @@ -14,31 +14,31 @@ 3. DNS support enabled on the VPC 4. Reverse Lookup zone (Route 53 PHZ) for the subnet is associated with the VPC. -It will then itereate through the Tags and based upon the Tag combination, +It will then itereate through the Tags and based upon the Tag combination, construct the A/PTR record to be created (running) or deleted (shutting down) -The following tag combination will result in the A/PTR record. +The following tag combination will result in the A/PTR record. The order matters since first match will skip the rest of the condition. 1. If Custom hostname Tag AND Custom Zone Tags exist and are valid - hostname: Custom Hostname - zonename: Custom Zonename 2. If Custom hostname Tag is valid AND there's NO Custom Zone Tag AND VPC Domain name is Valid - hostname: Custom Hostname -- zonename: VPC Domain name given via DHCP option +- zonename: VPC Domain name given via DHCP option (if custom hostname value contains fqdn, the zone is ignored) 3. If Name Tag is valid (both hostname and zonename portion) -- hostname: Name (minus the zone name) +- hostname: Name (minus the zone name) - zonename: Name (minus the host name) 4. If Name Tag is valid (only hostname portion) AND Custom Zone Tag is valid -- hostname: Name (minus the zone name) +- hostname: Name (minus the zone name) - zonename: Custom Zonename -5. If Name Tag is valid (only hostname portion) AND VPC Domain name is Valid -- hostname: Name (minus the zone name) +5. If Name Tag is valid (only hostname portion) AND VPC Domain name is Valid +- hostname: Name (minus the zone name) - zonename: VPC Domain name given via DHCP option 6. Custom Zone Tags is valid AND (Name hostname and custom hostname not Valid) - hostname: ip-1-2-3-4 format - zonename: Custom Zonename -7. No Custom Tags +7. No Custom Tags - hostname: ip-1-2-3-4 format - zonename: VPC Domain name given via DHCP option 8. If no match above then exit out script (no A/PTR record) @@ -65,12 +65,14 @@ import os import ipaddress from botocore.exceptions import ClientError +from collections import OrderedDict +from pprint import pformat # Setting Global Variables LOGGER = logging.getLogger() ACCOUNT = None REGION = None -VERSION = '0.0.11' +VERSION = '0.0.12' # Adjust the logging level [logging.INFO, logging.DEBUG, logging.WARNING, etc] LOGGER.setLevel(logging.DEBUG) @@ -179,8 +181,8 @@ def lambda_handler( if state == 'running': LOGGER.debug("sleeping for maximum {} seconds {}".format(SLEEPTIME, lineno())) - # wait increment and wait until maximum sleeptime - i = 1 + # wait increment and wait until maximum sleeptime + i = 1 while i < SLEEPTIME: LOGGER.debug("waiting count: %s", str(i) + lineno()) time.sleep(1) @@ -197,8 +199,8 @@ def lambda_handler( # if key attributes are found, then break out of the loop if all([t_private_ip, t_private_dns_name, t_subnet_id, t_vpc_id]): - LOGGER.debug ("instance data found, exiting while loop: " - "%s", t_private_dns_name + ","+ t_private_ip + ","+ t_subnet_id + ","+ t_vpc_id + lineno()) + LOGGER.debug("instance data found, exiting while loop: " + "%s", t_private_dns_name + "," + t_private_ip + "," + t_subnet_id + "," + t_vpc_id + lineno()) break except: LOGGER.info("no instance data, repeat check: %s", lineno()) @@ -360,7 +362,7 @@ def lambda_handler( # the VPC. Obtain the zone name, and check if there is a Private Hosted Zone # associated with the VPC. If so, it will set the zone name to be used later. has_dhcp_dns_zone_associated_vpc = False - + # store verified valid dns zones so to speed up the script. valid_dns_zones = [] @@ -396,7 +398,7 @@ def lambda_handler( if tag.get('Key').lstrip().lower() == TAGKEY_ZONE.lower(): LOGGER.debug("Zone Tag key: %s", tag.get('Key') + lineno()) - + # pause 1s to spread out API calls time.sleep(1) custom_zone_name = tag.get('Value').lstrip().lower() @@ -407,7 +409,7 @@ def lambda_handler( LOGGER.debug("Checking if custom_zone_name is valid: %s", str(custom_zone_name) + lineno()) - + # check if the zone is already validated, if not check if custom_zone_name in valid_dns_zones: LOGGER.debug("custom_zone_name already valid: %s", str( @@ -455,7 +457,7 @@ def lambda_handler( elif is_valid_zone(route53, cname_domain_suffix, hosted_zones, vpc_id, private_hosted_zone_collection): LOGGER.debug("cname domain is valid: %s", cname_domain_suffix + lineno()) - valid_dns_zones.append(cname_domain_suffix) + valid_dns_zones.append(cname_domain_suffix) has_valid_cname_tag = True else: LOGGER.debug("cname domain is not valid: %s", @@ -521,7 +523,7 @@ def lambda_handler( if name_domain_suffix in valid_dns_zones: has_valid_Name_tag_zonename = True LOGGER.debug("name_domain_suffix already valid: %s", str( - name_domain_suffix) + lineno()) + name_domain_suffix) + lineno()) elif is_valid_zone(route53, name_domain_suffix, hosted_zones, vpc_id, private_hosted_zone_collection): valid_dns_zones.append(name_domain_suffix) has_valid_Name_tag_zonename = True @@ -567,7 +569,8 @@ def lambda_handler( else: # none of the use-casem and no suitable zone to create the A record LOGGER.info("No DHCP Associated for VPC and no custom tags. Exiting Script") # nothing to do, exit out script - caller_response.append ('No DHCP Associated for VPC and no custom tags. Exiting Script') + caller_response.append( + 'No DHCP Associated for VPC and no custom tags. Exiting Script') return caller_response # put together the FQDN of the dns name... @@ -627,7 +630,7 @@ def lambda_handler( else: # not running so delete the records try: - + # pause 1 before deleting to avoid API limit time.sleep(1) delete_resource_record( @@ -1154,10 +1157,11 @@ def is_valid_hostname(hostname): except: LOGGER.info("unexpected error. %s\n", str(sys.exc_info()[0]) + lineno()) + def is_valid_zone(route53, zonename, hosted_zones, vpc_id, private_hosted_zone_collection): """ This function checks to see whether the zone "name" entered - is valid (PHZ zone exists and is associated with the VPC + is valid (PHZ zone exists and is associated with the VPC where instance is lauched in) :param zonename: :param vpc_id: @@ -1165,11 +1169,11 @@ def is_valid_zone(route53, zonename, hosted_zones, vpc_id, private_hosted_zone_c :param private_hosted_zone_collection: :return: """ - + LOGGER.debug("in function is_valid_zone") LOGGER.debug("Looking to validate zone: %s", zonename + lineno()) - + try: # check if the zone is PHZ if zonename.lower() in private_hosted_zone_collection: @@ -1396,3 +1400,113 @@ def get_subnet_cidr_block(client, subnet_id): return response['Subnets'][0]['CidrBlock'] except: LOGGER.info("unexpected error. %s\n", str(sys.exc_info()[0]) + lineno()) + + +def initialize_heritage(application_name, version='null', items={}): + """ + Initialize the heritage datastructure (dict). + :param str application_name: The application name. Shoud not have spaces. An empty application name will return an empty dict. + :param str version: A version of the specific implementation that created this. Versions are primarily for documenting what created the record TXT record. + :param dict(str) items: A dict of key/value pairs to set on initialization. They key of version is not permitted here. + :return dict(str): dict with the application name, version, and items ready for use + """ + if application_name != '' and appication_name is not none: + return { + 'application_name': str(appname), + 'version': str(version), + 'items': OrderedDict(items), + } + else: + return {} + + +def dump_heritage(data): + """ + Dump the heritage dict into a string. + :param dict(string): Dictionary containing heritage data + :return dict(string): string format of dict + """ + return pformat(data) + + +def add_heritage_item(data, key, value): + """ + Add a key/value pair to the heritage dict. + :param dict(string) data: Dictionary containing heritage data + :param str key: The key for the key/value pair + :param str value: The value for the key/value pair + :return: This adds the key/value pair to the heritage dict items. There is no return value. + """ + data['items'][str(key)] = str(value) + + +def add_heritage_item_timestamp(data, key): + """ + Add an epoch timestamp to the named field in key. + :param dict(string) data: Dictionary containing heritage data + :param str key: The key for the key to contain the timestamp + :return: This adds the key and current timestamp to the heritage dict items. There is no return value. + """ + data['items'][str(key)] = int(datetime.now().timestamp()) + + +def format_heritage(data): + """ + Return the TXT record format of the heritage data structure. This is of the format + heritage={app},{app}/version={version},{app}/{key}={value},... + + :param dict(string) data: Dictionary containing heritage data + :return str: This returns a string with the formatted heritage data comma separated + """ + appname = data['application_name'] + output = ['heritage={},{}/version={}'.format(appname, appname, data['version'])] + for k, v in data['items'].items(): + output.append('{}/{}={}'.format(appname, k, v)) + return ','.join(output) + + +def parse_heritage(info): + """ + Take a TXT record and parse it into a heritage dict. + :param str info: string with TXT record of heritage data + :return dict(str): Heritage dict + kv_results={} + kv=info.split(',') +# print(kv) + header=kv.pop(0).split('=') + + if header[0]!='heritage': + return kv_results + else: + appname=header[1] + kv_results['application_name']=appname + try: + for item in kv: + k,v=item.split('=',2) +# print('appname',appname,'k',k,'v',v) + if appname+'/' in k: + nk=k.replace(appname+'/','') + kv_results[nk]=v +# print('nk',nk) + if kv_results.get('version') is None: +# version=kv_result.pop('version') +# else: + version='null' +# return initialize_heritage(appname,version,kv_results) + return kv_results + except: + return {} + +# heritage examples to incorporate +# h=initialize_heritage('dynr53','0.0.9') +# pprint(h) +# add_heritage_item(h,'instance_id','i-123123123123') +# add_heritage_item(h,'account_id','123123123123') +# add_heritage_item(h,'region','west') +# add_heritage_item_timestamp(h,'create_time') +# txt=format_heritage(h) +# print(txt) +# nh=parse_heritage(txt) +# print(dump_heritage(nh)) +# nh=parse_heritage('bob'+txt) +# print(dump_heritage(nh)) diff --git a/code/ddns-lambda.zip b/code/ddns-lambda.zip index c0c6635..89902c6 100644 Binary files a/code/ddns-lambda.zip and b/code/ddns-lambda.zip differ diff --git a/version.tf b/version.tf index 56bf4db..3ca2fc8 100644 --- a/version.tf +++ b/version.tf @@ -1,3 +1,3 @@ locals { - _module_version = "0.0.20" + _module_version = "0.0.21" }