Skip to content

Commit

Permalink
Merge pull request #12 from terraform-modules/feature-heritage
Browse files Browse the repository at this point in the history
add heritage functions
  • Loading branch information
badra001 committed Feb 17, 2022
2 parents 20299f6 + e7099b3 commit 2d621b3
Show file tree
Hide file tree
Showing 4 changed files with 142 additions and 25 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
162 changes: 138 additions & 24 deletions code/ddns-lambda.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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())
Expand Down Expand Up @@ -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 = []

Expand Down Expand Up @@ -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()
Expand All @@ -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(
Expand Down Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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...
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -1154,22 +1157,23 @@ 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:
:param route53:
: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:
Expand Down Expand Up @@ -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))
Binary file modified code/ddns-lambda.zip
Binary file not shown.
2 changes: 1 addition & 1 deletion version.tf
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
locals {
_module_version = "0.0.20"
_module_version = "0.0.21"
}

0 comments on commit 2d621b3

Please sign in to comment.