"""State module for managing Route53 Resource records."""
import copy
from typing import Any
from typing import Dict
from typing import List
__contracts__ = ["resource"]
[docs]async def present(
hub,
ctx,
name: str,
hosted_zone_id: str,
record_type: str,
resource_records: List[str] = None,
alias_target: Dict = None,
resource_id: str = None,
ttl: int = None,
) -> Dict[str, Any]:
"""Creates or changes a resource record set, which contains authoritative DNS information for a specified domain name
or subdomain name.
Args:
name(str):
The name of the record. A (``.``) will be appended if not already present.
hosted_zone_id(str):
The ID of the hosted zone that contains the resource record sets.
record_type(str):
The DNS record type. For information about different record types and how data is encoded for
them, see `Supported DNS Resource Record Types` https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/ResourceRecordTypes.html
in the Amazon Route 53 Developer Guide.
resource_records(list, Optional):
Information about the resource records to act upon.
alias_target(dict, Optional):
Alias resource record sets only: Information about the Amazon Web Services resource, such as a CloudFront
distribution or an Amazon S3 bucket, that you want to route traffic to.
resource_id(str, Optional):
Composite ID for a resource record in a hosted zone. String formatted as
<hosted_zone_id>_<record_name>_<record_type>.
ttl(int, Optional):
The resource record cache time to live (TTL), in seconds.
Returns:
Dict[str, Any]
Examples:
.. code-block:: sls
KL1PX9DBMUY9WHB_www.example.com_AAAA:
aws.route53.resource_record.present:
- hosted_zone_id: /hostedzone/KL1PX9DBMUY9WHB
- name: www.example.com.
- record_type: AAAA
- resource_records:
- 2001:0db8:85a3:0:0:8a2e:0370:7335
- 2001:0db8:85a3:0:0:8a2e:0370:7334
- ttl: 300
ZY51FUS5VYB_www.example.net_A:
aws.route53.resource_record.present:
- alias_target:
dns_name: lb1.us-east-1.elb.amazonaws.com.
evaluate_target_health: false
hosted_zone_id: Z35SXDOTRQ7X7Z
- hosted_zone_id: /hostedzone/ZY51FUS5VYB
- name: www.example.net.
- record_type: A
- resource_id: ZY51FUS5VYB_www.example.net_A
"""
result = dict(comment=[], old_state=None, new_state=None, name=name, result=True)
# name is supposed to end with '.'.
# Because name is used when we look up the resource record we should store the canonical form in the state
formatted_name = name if name.endswith(".") else f"{name}."
if resource_id:
if (
hub.tool.aws.route53.resource_record_utils.compose_resource_id(
hosted_zone_id, name, record_type
)
!= resource_id
):
result["comment"] = (
f"aws.route53.resource_record resource_id {resource_id} is not composed of {hosted_zone_id}, {name} and {record_type}",
)
result["result"] = False
return result
before_response = (
await hub.tool.aws.route53.resource_record_utils.find_resource_record(
ctx, hosted_zone_id, formatted_name, record_type
)
)
if not before_response["result"]:
result["comment"] = before_response["comment"]
result["result"] = False
return result
before = before_response.get("aws_state")
pending_state = None
if before:
result[
"old_state"
] = hub.tool.aws.route53.conversion_utils.convert_raw_resource_record_to_present(
hosted_zone_id=hosted_zone_id, raw_resource=before
)
pending_state = hub.tool.aws.route53.resource_record_utils.patch_state(
before, hosted_zone_id, ttl, resource_records, alias_target
)
else:
pending_state = {
"name": formatted_name,
"hosted_zone_id": hosted_zone_id,
"resource_id": resource_id
if resource_id
else hub.tool.aws.route53.resource_record_utils.compose_resource_id(
hosted_zone_id, formatted_name, record_type
),
"record_type": record_type,
}
if ttl:
pending_state["ttl"] = ttl
if resource_records:
pending_state["resource_records"] = resource_records
if alias_target:
pending_state["alias_target"] = alias_target
# action can be 'CREATE'|'DELETE'|'UPSERT' - see boto documentation for change_resource_record_sets
action = None
if before:
if not hub.tool.aws.route53.resource_record_utils.same_states(
pending_state, result["old_state"]
):
result["comment"] = hub.tool.aws.comment_utils.would_update_comment(
resource_type="aws.route53.resource_record", name=formatted_name
)
action = "UPSERT"
else:
result["comment"] += [
f"No changes would be made for aws.route53.resource_record {formatted_name}"
]
result["result"] = True
else:
result["comment"] = hub.tool.aws.comment_utils.would_create_comment(
resource_type="aws.route53.resource_record", name=formatted_name
)
action = "CREATE"
result["result"] = True
if ctx.get("test", False):
result["new_state"] = pending_state
return result
# do the actual update
if action:
change_batch = (
hub.tool.aws.route53.resource_record_utils.create_change_batch_for_update(
action, pending_state
)
)
response = await hub.exec.boto3.client.route53.change_resource_record_sets(
ctx, HostedZoneId=hosted_zone_id, ChangeBatch=change_batch
)
if not response["result"]:
result["comment"] += [
f'Error on {action} for aws.route53.resource_record {formatted_name}: {response["comment"]}'
]
result["result"] = False
return result
result["new_state"] = pending_state
if action == "CREATE":
result["comment"] = hub.tool.aws.comment_utils.create_comment(
resource_type="aws.route53.resource_record", name=formatted_name
)
else:
result["comment"] = hub.tool.aws.comment_utils.update_comment(
resource_type="aws.route53.resource_record", name=formatted_name
)
else:
result["new_state"] = copy.deepcopy(result["old_state"])
return result
[docs]async def absent(hub, ctx, name: str, resource_id: str = None) -> Dict[str, Any]:
"""Deletes the specified resource record
Args:
name(str):
Name of the resource record. Needed because of the Idem contract but not used.
resource_id(str, Optional):
Composite ID for a resource record in a hosted zone. String formatted as
<hosted_zone_id>_<record_name>_<record_type>.
Returns:
Dict[str, Any]
Examples:
.. code-block:: sls
MX_record_is_absent:
aws.route53.resource_record.absent:
- name: www.example.com.
- resource_id: HSHMRK8IGWBU3PU_www.example.com_MX
"""
result = dict(comment=[], old_state=None, new_state=None, name=name, result=True)
if not resource_id:
result["comment"] = hub.tool.aws.comment_utils.already_absent_comment(
resource_type="aws.route53.resource_record", name=name
)
return result
try:
decomposed_id = (
hub.tool.aws.route53.resource_record_utils.decompose_resource_id(
resource_id
)
)
except Exception as e:
result["comment"] += [f"{e.__class__.__name__}: {e}"]
result["result"] = False
return result
hosted_zone_id = decomposed_id["hosted_zone_id"]
formatted_name = f"{decomposed_id['name']}"
record_type = decomposed_id["record_type"]
before_response = (
await hub.tool.aws.route53.resource_record_utils.find_resource_record(
ctx,
hosted_zone_id,
formatted_name,
record_type,
)
)
if not before_response["result"]:
result["comment"] = before_response["comment"]
result["result"] = False
return result
before = before_response.get("aws_state")
if not before:
result["comment"] = hub.tool.aws.comment_utils.already_absent_comment(
resource_type="aws.route53.resource_record", name=formatted_name
)
else:
result[
"old_state"
] = hub.tool.aws.route53.conversion_utils.convert_raw_resource_record_to_present(
hosted_zone_id=hosted_zone_id, raw_resource=before
)
result["comment"] = hub.tool.aws.comment_utils.would_delete_comment(
resource_type="aws.route53.resource_record", name=formatted_name
)
if ctx.get("test", False):
return result
# delete the resource record
change_batch = (
hub.tool.aws.route53.resource_record_utils.create_change_batch_for_update(
"DELETE", result["old_state"]
)
)
response = await hub.exec.boto3.client.route53.change_resource_record_sets(
ctx,
HostedZoneId=hosted_zone_id,
ChangeBatch=change_batch,
)
if not response["result"]:
result["comment"] += [
f"Error deleting aws.route53.resource_record '{formatted_name}': {response['comment']}"
]
result["result"] = False
return result
result["comment"] = hub.tool.aws.comment_utils.delete_comment(
resource_type="aws.route53.resource_record", name=formatted_name
)
return result
[docs]async def describe(hub, ctx) -> Dict[str, Dict[str, Any]]:
"""
Describe the resource in a way that can be recreated/managed with the corresponding "present" function
Lists the resource record sets in a specified hosted zone.
Returns:
Dict[str, Any]
Examples:
.. code-block:: bash
$ idem describe aws.route53.resource_record
"""
result = {}
hosted_zones = await hub.exec.boto3.client.route53.list_hosted_zones(ctx)
if not hosted_zones["result"]:
hub.log.warning(
f"Could not describe aws.route53.resource_record. {hosted_zones['comment']}"
)
return result
for hosted_zone in hosted_zones["ret"]["HostedZones"]:
hosted_zone_id = hosted_zone.get("Id")
ret = await hub.exec.boto3.client.route53.list_resource_record_sets(
ctx, HostedZoneId=hosted_zone_id
)
if not ret["result"]:
hub.log.warning(
f"Could not describe aws.route53.hosted_zone {ret['comment']}"
)
continue
for resource_record_set in ret["ret"]["ResourceRecordSets"]:
resource_translated = hub.tool.aws.route53.conversion_utils.convert_raw_resource_record_to_present(
hosted_zone_id=hosted_zone_id, raw_resource=resource_record_set
)
result[resource_translated["resource_id"]] = {
"aws.route53.resource_record.present": [
{parameter_key: parameter_value}
for parameter_key, parameter_value in resource_translated.items()
]
}
return result