From b8f9f920139df8655058ae5a023a80c800627fd9 Mon Sep 17 00:00:00 2001 From: badra001 Date: Mon, 7 Feb 2022 14:55:18 -0500 Subject: [PATCH] fix version string --- code/ddns-lambda.py | 175 ++++++++++++++++++++++--------------------- code/ddns-lambda.zip | Bin 8783 -> 8757 bytes 2 files changed, 91 insertions(+), 84 deletions(-) diff --git a/code/ddns-lambda.py b/code/ddns-lambda.py index 9e8bd49..e8ba0f5 100755 --- a/code/ddns-lambda.py +++ b/code/ddns-lambda.py @@ -62,7 +62,7 @@ LOGGER = logging.getLogger() ACCOUNT = None REGION = None -VERSION = '0.0.7' +VERSION = '0.0.8' # Adjust the logging level [logging.INFO, logging.DEBUG, logging.WARNING, etc] LOGGER.setLevel(logging.DEBUG) @@ -76,7 +76,7 @@ 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( +print('Loading function v{}: {}'.format( VERSION, datetime.datetime.now().time().isoformat())) @@ -87,6 +87,7 @@ def lineno(): # pragma: no cover """ return str(' - line number: ' + str(inspect.currentframe().f_back.f_lineno)) + def get_route53_client(): """ Get route53 client @@ -272,7 +273,7 @@ def lambda_handler( # 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. + # the reverse lookup zone is associated with the instance's VPC. LOGGER.info("reversed_lookup_zone: %s", str(reversed_lookup_zone) + lineno()) reverse_zone = None for record in hosted_zones['HostedZones']: @@ -328,7 +329,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 - + for configuration in dhcp_configurations: LOGGER.debug("configuration: %s", str(configuration) + lineno()) LOGGER.debug("private hosted zones: %s", str( @@ -343,13 +344,13 @@ def lambda_handler( 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 - ) + route53, + private_hosted_zone_id + ) LOGGER.debug("private_hosted_zone_properties:" " %s", str(private_hosted_zone_properties) + lineno()) - + has_dhcp_dns_zone_associated_vpc = True LOGGER.debug("iterating through tags %s", lineno()) @@ -365,16 +366,17 @@ def lambda_handler( LOGGER.debug("#### tag: %s", str(tag) + lineno()) if tag.get('Key').lstrip().lower() == TAGKEY_ZONE.lower(): - LOGGER.debug("Zone Tag key: %s", tag.get('Key') + lineno()) + LOGGER.debug("Zone Tag key: %s", tag.get('Key') + lineno()) custom_zone_name = tag.get('Value').lstrip().lower() - if custom_zone_name[-1] != '.': # add a trailing period if it does not have it. + # add a trailing period if it does not have it. + if custom_zone_name[-1] != '.': custom_zone_name = custom_zone_name + '.' # check if the zone is PHZ VPC associated if custom_zone_name in private_hosted_zone_collection: LOGGER.debug("Private zone found: %s", - str(custom_zone_name) + lineno()) + str(custom_zone_name) + lineno()) zone_tag_hosted_zone_name = custom_zone_name LOGGER.debug("zone_tag_hosted_zone_name: %s", str( @@ -383,20 +385,20 @@ def lambda_handler( has_valid_zone_tag = True else: LOGGER.debug("Private zone NOT found: %s", - str(custom_zone_name) + lineno()) + str(custom_zone_name) + lineno()) elif tag.get('Key').lstrip().lower() == TAGKEY_CNAME.lower(): LOGGER.debug("CNAME Tag key: %s", tag.get('Key') + lineno()) if is_valid_hostname(tag.get('Value')): - + LOGGER.debug("CNAME hostname of %s is valid %s", str(tag.get('Value')), lineno()) - + # convert the cname value to lower case and strip whitespace and newline characters icname = tag.get('Value').lstrip().lower() - if icname[-1] != '.': # add a trailing period if it does not have it. + if icname[-1] != '.': # add a trailing period if it does not have it. icname = icname + '.' LOGGER.debug("icname: %s", str(icname) + lineno()) @@ -417,12 +419,12 @@ def lambda_handler( # check if the zone is PHZ VPC associated if cname_domain_suffix.lower() in private_hosted_zone_collection: LOGGER.debug("Private zone found: %s", - str(tag.get('Value')) + lineno()) + str(tag.get('Value')) + lineno()) has_valid_cname_tag = True else: LOGGER.debug("cname domain is not associated with vpc: %s", - cname_domain_suffix + lineno()) + cname_domain_suffix + lineno()) elif tag.get('Key').lstrip().lower() == TAGKEY_HOSTNAME.lower(): LOGGER.debug("Custom Hostname Tag key: %s", tag.get('Key') + lineno()) @@ -452,66 +454,70 @@ def lambda_handler( # convert the hostname value to lower case and strip whitespace and newline characters name_value = tag.get('Value').lstrip().lower() - if name_value[-1] != '.': # add a trailing period if it does not have it. + # add a trailing period if it does not have it. + if name_value[-1] != '.': name_value = name_value + '.' LOGGER.debug("Name: %s", str(name_value) + lineno()) - + # Gets the host and the zone name (split up based) name_host = name_value.split('.')[0] LOGGER.debug("name_host: %s", str( - name_host) + lineno()) + name_host) + lineno()) name_domain_suffix = name_value[name_value.find('.') + 1:] LOGGER.debug("name_domain_suffix: %s", str( - name_domain_suffix) + lineno()) - + name_domain_suffix) + lineno()) + # check if the zone name == dhcp zone if name_domain_suffix == private_hosted_zone_name: - LOGGER.debug("name_domain_suffix is the same as DHCP dns: %s", str(name_domain_suffix) + lineno()) + LOGGER.debug("name_domain_suffix is the same as DHCP dns: %s", str( + name_domain_suffix) + lineno()) has_valid_Name_tag = True else: - LOGGER.debug("name_domain_suffix is NOT the same as DHCP dns: %s", str(name_domain_suffix) + lineno()) + LOGGER.debug("name_domain_suffix is NOT the same as DHCP dns: %s", str( + name_domain_suffix) + lineno()) else: LOGGER.debug("Name of %s is invalid %s", str(tag.get('Value')), lineno()) - else: - LOGGER.debug("Skipping Tag key: %s", tag.get('Key') + lineno()) + else: + LOGGER.debug("Skipping Tag key: %s", tag.get('Key') + lineno()) # determine correct A/PTR record to be created based upon the boolean values from the tags above - if has_valid_hostname_tag and has_valid_zone_tag: - LOGGER.info("custom hostname tag and custom zone tag valid.") - final_private_hostname = custom_host_name + if has_valid_hostname_tag and has_valid_zone_tag: + LOGGER.info("custom hostname tag and custom zone tag valid.") + final_private_hostname = custom_host_name final_hosted_zone_name = zone_tag_hosted_zone_name - elif has_valid_hostname_tag and not (has_valid_zone_tag) and has_dhcp_dns_zone_associated_vpc: #3 - LOGGER.info("custom hostname valid only.") - final_private_hostname = custom_host_name + elif has_valid_hostname_tag and not (has_valid_zone_tag) and has_dhcp_dns_zone_associated_vpc: # 3 + LOGGER.info("custom hostname valid only.") + final_private_hostname = custom_host_name final_hosted_zone_name = private_hosted_zone_name elif has_valid_Name_tag and has_dhcp_dns_zone_associated_vpc: - LOGGER.info("Name tag valid.") - final_private_hostname = name_host + LOGGER.info("Name tag valid.") + final_private_hostname = name_host final_hosted_zone_name = private_hosted_zone_name elif has_valid_zone_tag and not (has_valid_hostname_tag): - LOGGER.info("custom zone tag valid.") - final_private_hostname = private_host_name + LOGGER.info("custom zone tag valid.") + final_private_hostname = private_host_name final_hosted_zone_name = zone_tag_hosted_zone_name - elif has_dhcp_dns_zone_associated_vpc: - LOGGER.info("no custom tags - use default.") - final_private_hostname = private_host_name + elif has_dhcp_dns_zone_associated_vpc: + LOGGER.info("no custom tags - use default.") + final_private_hostname = private_host_name final_hosted_zone_name = private_hosted_zone_name - else: # none of the use-casem and no suitable zone to create the A record - LOGGER.info("No DHCP Associated for VPC and no custom tags. Exiting Script") + else: # none of the use-casem and no suitable zone to create the A record + LOGGER.info("No DHCP Associated for VPC and no custom tags. Exiting Script") # nothing to do, exit out script exit(-1) # put together the FQDN of the dns name... final_private_dns_name = final_private_hostname + '.' + final_hosted_zone_name - LOGGER.info("final hostname for A and PTR record: %s", str(final_private_dns_name) + lineno()) + LOGGER.info("final hostname for A and PTR record: %s", + str(final_private_dns_name) + lineno()) # Get the PHZ ID for the Zone final_hosted_zone_id = get_zone_id( route53, final_hosted_zone_name) LOGGER.debug("private_hosted_zone_id:" - " %s", str(final_hosted_zone_id) + lineno()) + " %s", str(final_hosted_zone_id) + lineno()) # Create OR Delete the A / PTR Record if state == 'running': @@ -528,12 +534,12 @@ def lambda_handler( ) caller_response.append('Created A record in zone id: ' + - str(final_hosted_zone_id) + - ' for hosted zone ' + - str(final_private_hostname) + '.' + - str(final_hosted_zone_name) + - ' with value: ' + - str(private_ip)) + str(final_hosted_zone_id) + + ' for hosted zone ' + + str(final_private_hostname) + '.' + + str(final_hosted_zone_name) + + ' with value: ' + + str(private_ip)) if reverse_zone_associated: create_resource_record( @@ -546,16 +552,16 @@ def lambda_handler( ) caller_response.append('Created PTR record in zone id: ' + - str(reverse_lookup_zone_id) + - ' for hosted zone ' + - str(reversed_ip_address) + - 'in-addr.arpa with value: ' + - str(final_private_dns_name)) + str(reverse_lookup_zone_id) + + ' for hosted zone ' + + str(reversed_ip_address) + + 'in-addr.arpa with value: ' + + str(final_private_dns_name)) except BaseException as err: LOGGER.info("unexpected error. %s\n", str(err) + lineno()) - else: # not running so delete the records + else: # not running so delete the records try: delete_resource_record( route53, @@ -567,12 +573,12 @@ def lambda_handler( ) caller_response.append('Deleted A record in zone id: ' + - str(final_hosted_zone_id) + - ' for hosted zone ' + - str(final_private_hostname) + '.' + - str(final_hosted_zone_name) + - ' with value: ' + - str(private_ip)) + str(final_hosted_zone_id) + + ' for hosted zone ' + + str(final_private_hostname) + '.' + + str(final_hosted_zone_name) + + ' with value: ' + + str(private_ip)) delete_resource_record( route53, @@ -584,12 +590,12 @@ def lambda_handler( ) caller_response.append('Deleted PTR record in zone id: ' + - str(reverse_lookup_zone_id) + - ' for hosted zone ' + - str(reversed_ip_address) + - str(private_dns_name) + - ' with value: ' + - str(final_private_dns_name)) + str(reverse_lookup_zone_id) + + ' for hosted zone ' + + str(reversed_ip_address) + + str(private_dns_name) + + ' with value: ' + + str(final_private_dns_name)) except BaseException as err: LOGGER.debug("%s", str(err) + lineno()) @@ -597,17 +603,17 @@ def lambda_handler( # Create the CNAME record only if it has passed the check if has_valid_cname_tag: LOGGER.debug("cname record is valid - creating CNAME record:" - " %s", str(cname_host_name) + "." + str(cname_domain_suffix) + lineno()) + " %s", str(cname_host_name) + "." + str(cname_domain_suffix) + lineno()) # create CNAME record in private zone if state == 'running': try: LOGGER.debug("cname_host_name:" - " %s", str(cname_host_name) + lineno()) + " %s", str(cname_host_name) + lineno()) LOGGER.debug("cname_domain_suffix:" - " %s", str(cname_domain_suffix) + lineno()) + " %s", str(cname_domain_suffix) + lineno()) LOGGER.debug("cname_domain_suffix_id:" - " %s", str(cname_domain_suffix_id) + lineno()) + " %s", str(cname_domain_suffix_id) + lineno()) create_resource_record( route53, @@ -619,12 +625,12 @@ def lambda_handler( ) caller_response.append('Created CNAME record in zone id: ' + - str(cname_domain_suffix_id) + - ' for hosted zone ' + - str(cname_host_name) + '.' + - str(cname_domain_suffix) + - ' with value: ' + - str(final_private_dns_name)) + str(cname_domain_suffix_id) + + ' for hosted zone ' + + str(cname_host_name) + '.' + + str(cname_domain_suffix) + + ' with value: ' + + str(final_private_dns_name)) except BaseException as err: LOGGER.debug("%s", str(err) + lineno()) @@ -642,12 +648,12 @@ def lambda_handler( ) caller_response.append('Deleted CNAME record in zone id: ' + - str(cname_domain_suffix_id) + - ' for hosted zone ' + - str(cname_host_name) + '.' + - str(cname_domain_suffix) + - ' with value: ' + - str(final_private_dns_name)) + str(cname_domain_suffix_id) + + ' for hosted zone ' + + str(cname_host_name) + '.' + + str(cname_domain_suffix) + + ' with value: ' + + str(final_private_dns_name)) except BaseException as err: LOGGER.debug("%s", str(err) + lineno()) @@ -873,6 +879,7 @@ def get_dynamodb_table(client, table_name): except ClientError as err: LOGGER.info("unexpected error. %s\n", str(err) + lineno()) + def change_resource_recordset(client, zone_id, host_name, hosted_zone_name, record_type, value): """ Change resource recordset @@ -1093,7 +1100,7 @@ def get_dhcp_configurations(client, dhcp_options_id): dhcp_configurations = response['DhcpOptions'][0]['DhcpConfigurations'] LOGGER.debug("dhcp_configurations: %s", str(dhcp_configurations) + lineno()) for configuration in dhcp_configurations: - if configuration['Key']=='domain-name': # only if the key is domain-name + if configuration['Key'] == 'domain-name': # only if the key is domain-name for item in configuration['Values']: LOGGER.debug("item: %s", str(item) + lineno()) zone_names.append(str(item['Value']) + '.') diff --git a/code/ddns-lambda.zip b/code/ddns-lambda.zip index 7609ad334d0d6f1894eaec7b08d033a7a16caacf..1c86ecd2a0c4102f89e9f3ab101e03c4be93f2de 100644 GIT binary patch literal 8757 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 XKt&kX|7}D3SDOFz$^YOG0O0=sugA8R literal 8783 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