2021-10-22 21:47:29 +00:00
|
|
|
"""Route53Backend class with methods for supported APIs."""
|
2021-04-20 11:50:05 +00:00
|
|
|
import itertools
|
2016-09-21 00:41:23 +00:00
|
|
|
from collections import defaultdict
|
2021-10-22 21:47:29 +00:00
|
|
|
import re
|
2016-09-21 00:41:23 +00:00
|
|
|
|
2017-09-22 22:50:01 +00:00
|
|
|
import string
|
|
|
|
import random
|
2015-01-18 00:06:43 +00:00
|
|
|
import uuid
|
2015-01-17 19:50:19 +00:00
|
|
|
from jinja2 import Template
|
|
|
|
|
2021-10-22 21:47:29 +00:00
|
|
|
from moto.route53.exceptions import (
|
|
|
|
InvalidInput,
|
|
|
|
NoSuchCloudWatchLogsLogGroup,
|
|
|
|
NoSuchHostedZone,
|
|
|
|
NoSuchQueryLoggingConfig,
|
|
|
|
QueryLoggingConfigAlreadyExists,
|
|
|
|
)
|
|
|
|
from moto.core import BaseBackend, BaseModel, CloudFormationModel
|
|
|
|
from moto.utilities.paginator import paginate
|
|
|
|
from .utils import PAGINATION_MODEL
|
2017-09-22 22:50:01 +00:00
|
|
|
|
|
|
|
ROUTE53_ID_CHOICE = string.ascii_uppercase + string.digits
|
|
|
|
|
|
|
|
|
|
|
|
def create_route53_zone_id():
|
|
|
|
# New ID's look like this Z1RWWTK7Y8UDDQ
|
2019-10-31 15:44:26 +00:00
|
|
|
return "".join([random.choice(ROUTE53_ID_CHOICE) for _ in range(0, 15)])
|
2013-11-14 19:14:14 +00:00
|
|
|
|
|
|
|
|
2020-08-01 14:23:36 +00:00
|
|
|
class HealthCheck(CloudFormationModel):
|
2021-08-22 09:51:21 +00:00
|
|
|
def __init__(self, health_check_id, caller_reference, health_check_args):
|
2015-01-18 00:06:43 +00:00
|
|
|
self.id = health_check_id
|
|
|
|
self.ip_address = health_check_args.get("ip_address")
|
2021-08-22 09:51:21 +00:00
|
|
|
self.port = health_check_args.get("port") or 80
|
2019-02-27 10:54:55 +00:00
|
|
|
self.type_ = health_check_args.get("type")
|
2015-01-18 00:06:43 +00:00
|
|
|
self.resource_path = health_check_args.get("resource_path")
|
|
|
|
self.fqdn = health_check_args.get("fqdn")
|
|
|
|
self.search_string = health_check_args.get("search_string")
|
2021-08-22 09:51:21 +00:00
|
|
|
self.request_interval = health_check_args.get("request_interval") or 30
|
|
|
|
self.failure_threshold = health_check_args.get("failure_threshold") or 3
|
|
|
|
self.health_threshold = health_check_args.get("health_threshold")
|
|
|
|
self.measure_latency = health_check_args.get("measure_latency") or False
|
|
|
|
self.inverted = health_check_args.get("inverted") or False
|
|
|
|
self.disabled = health_check_args.get("disabled") or False
|
|
|
|
self.enable_sni = health_check_args.get("enable_sni") or False
|
|
|
|
self.children = health_check_args.get("children") or None
|
|
|
|
self.caller_reference = caller_reference
|
2015-01-18 00:06:43 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def physical_resource_id(self):
|
|
|
|
return self.id
|
|
|
|
|
2020-08-01 14:23:36 +00:00
|
|
|
@staticmethod
|
|
|
|
def cloudformation_name_type():
|
|
|
|
return None
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def cloudformation_type():
|
|
|
|
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-route53-healthcheck.html
|
|
|
|
return "AWS::Route53::HealthCheck"
|
|
|
|
|
2015-01-18 00:06:43 +00:00
|
|
|
@classmethod
|
2019-10-31 15:44:26 +00:00
|
|
|
def create_from_cloudformation_json(
|
2021-11-03 21:00:42 +00:00
|
|
|
cls, resource_name, cloudformation_json, region_name, **kwargs
|
2019-10-31 15:44:26 +00:00
|
|
|
):
|
|
|
|
properties = cloudformation_json["Properties"]["HealthCheckConfig"]
|
2015-01-18 00:06:43 +00:00
|
|
|
health_check_args = {
|
2019-10-31 15:44:26 +00:00
|
|
|
"ip_address": properties.get("IPAddress"),
|
|
|
|
"port": properties.get("Port"),
|
|
|
|
"type": properties["Type"],
|
|
|
|
"resource_path": properties.get("ResourcePath"),
|
|
|
|
"fqdn": properties.get("FullyQualifiedDomainName"),
|
|
|
|
"search_string": properties.get("SearchString"),
|
|
|
|
"request_interval": properties.get("RequestInterval"),
|
|
|
|
"failure_threshold": properties.get("FailureThreshold"),
|
2015-01-18 00:06:43 +00:00
|
|
|
}
|
2021-08-22 09:51:21 +00:00
|
|
|
health_check = route53_backend.create_health_check(
|
|
|
|
caller_reference=resource_name, health_check_args=health_check_args
|
|
|
|
)
|
2015-01-18 00:06:43 +00:00
|
|
|
return health_check
|
|
|
|
|
|
|
|
def to_xml(self):
|
2019-10-31 15:44:26 +00:00
|
|
|
template = Template(
|
|
|
|
"""<HealthCheck>
|
2015-01-18 00:06:43 +00:00
|
|
|
<Id>{{ health_check.id }}</Id>
|
2021-08-22 09:51:21 +00:00
|
|
|
<CallerReference>{{ health_check.caller_reference }}</CallerReference>
|
2015-01-18 00:06:43 +00:00
|
|
|
<HealthCheckConfig>
|
2021-08-22 09:51:21 +00:00
|
|
|
{% if health_check.type_ != "CALCULATED" %}
|
|
|
|
<IPAddress>{{ health_check.ip_address }}</IPAddress>
|
|
|
|
<Port>{{ health_check.port }}</Port>
|
|
|
|
{% endif %}
|
2019-02-27 10:54:55 +00:00
|
|
|
<Type>{{ health_check.type_ }}</Type>
|
2021-08-22 09:51:21 +00:00
|
|
|
{% if health_check.resource_path %}
|
|
|
|
<ResourcePath>{{ health_check.resource_path }}</ResourcePath>
|
|
|
|
{% endif %}
|
|
|
|
{% if health_check.fqdn %}
|
|
|
|
<FullyQualifiedDomainName>{{ health_check.fqdn }}</FullyQualifiedDomainName>
|
|
|
|
{% endif %}
|
|
|
|
{% if health_check.type_ != "CALCULATED" %}
|
|
|
|
<RequestInterval>{{ health_check.request_interval }}</RequestInterval>
|
|
|
|
<FailureThreshold>{{ health_check.failure_threshold }}</FailureThreshold>
|
|
|
|
<MeasureLatency>{{ health_check.measure_latency }}</MeasureLatency>
|
|
|
|
{% endif %}
|
|
|
|
{% if health_check.type_ == "CALCULATED" %}
|
|
|
|
<HealthThreshold>{{ health_check.health_threshold }}</HealthThreshold>
|
|
|
|
{% endif %}
|
|
|
|
<Inverted>{{ health_check.inverted }}</Inverted>
|
|
|
|
<Disabled>{{ health_check.disabled }}</Disabled>
|
|
|
|
<EnableSNI>{{ health_check.enable_sni }}</EnableSNI>
|
2015-01-18 00:06:43 +00:00
|
|
|
{% if health_check.search_string %}
|
|
|
|
<SearchString>{{ health_check.search_string }}</SearchString>
|
|
|
|
{% endif %}
|
2021-08-22 09:51:21 +00:00
|
|
|
{% if health_check.children %}
|
|
|
|
<ChildHealthChecks>
|
|
|
|
{% for child in health_check.children %}
|
|
|
|
<member>{{ child }}</member>
|
|
|
|
{% endfor %}
|
|
|
|
</ChildHealthChecks>
|
|
|
|
{% endif %}
|
2015-01-18 00:06:43 +00:00
|
|
|
</HealthCheckConfig>
|
|
|
|
<HealthCheckVersion>1</HealthCheckVersion>
|
2019-10-31 15:44:26 +00:00
|
|
|
</HealthCheck>"""
|
|
|
|
)
|
2015-01-18 00:06:43 +00:00
|
|
|
return template.render(health_check=self)
|
|
|
|
|
|
|
|
|
2020-08-01 14:23:36 +00:00
|
|
|
class RecordSet(CloudFormationModel):
|
2015-01-17 19:50:19 +00:00
|
|
|
def __init__(self, kwargs):
|
2019-10-31 15:44:26 +00:00
|
|
|
self.name = kwargs.get("Name")
|
|
|
|
self.type_ = kwargs.get("Type")
|
|
|
|
self.ttl = kwargs.get("TTL")
|
|
|
|
self.records = kwargs.get("ResourceRecords", [])
|
|
|
|
self.set_identifier = kwargs.get("SetIdentifier")
|
|
|
|
self.weight = kwargs.get("Weight")
|
|
|
|
self.region = kwargs.get("Region")
|
|
|
|
self.health_check = kwargs.get("HealthCheckId")
|
|
|
|
self.hosted_zone_name = kwargs.get("HostedZoneName")
|
|
|
|
self.hosted_zone_id = kwargs.get("HostedZoneId")
|
|
|
|
self.alias_target = kwargs.get("AliasTarget")
|
2020-03-02 12:46:15 +00:00
|
|
|
self.failover = kwargs.get("Failover")
|
|
|
|
self.geo_location = kwargs.get("GeoLocation")
|
2015-01-17 19:50:19 +00:00
|
|
|
|
2020-08-01 14:23:36 +00:00
|
|
|
@staticmethod
|
|
|
|
def cloudformation_name_type():
|
|
|
|
return "Name"
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def cloudformation_type():
|
|
|
|
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-route53-recordset.html
|
|
|
|
return "AWS::Route53::RecordSet"
|
|
|
|
|
2015-01-17 20:37:46 +00:00
|
|
|
@classmethod
|
2019-10-31 15:44:26 +00:00
|
|
|
def create_from_cloudformation_json(
|
2021-11-03 21:00:42 +00:00
|
|
|
cls, resource_name, cloudformation_json, region_name, **kwargs
|
2019-10-31 15:44:26 +00:00
|
|
|
):
|
|
|
|
properties = cloudformation_json["Properties"]
|
2015-01-17 20:37:46 +00:00
|
|
|
|
2016-03-07 19:25:25 +00:00
|
|
|
zone_name = properties.get("HostedZoneName")
|
|
|
|
if zone_name:
|
|
|
|
hosted_zone = route53_backend.get_hosted_zone_by_name(zone_name)
|
|
|
|
else:
|
2019-10-31 15:44:26 +00:00
|
|
|
hosted_zone = route53_backend.get_hosted_zone(properties["HostedZoneId"])
|
2015-01-17 20:37:46 +00:00
|
|
|
record_set = hosted_zone.add_rrset(properties)
|
|
|
|
return record_set
|
|
|
|
|
2016-04-28 13:21:54 +00:00
|
|
|
@classmethod
|
2019-10-31 15:44:26 +00:00
|
|
|
def update_from_cloudformation_json(
|
|
|
|
cls, original_resource, new_resource_name, cloudformation_json, region_name
|
|
|
|
):
|
2017-02-24 02:37:43 +00:00
|
|
|
cls.delete_from_cloudformation_json(
|
2019-10-31 15:44:26 +00:00
|
|
|
original_resource.name, cloudformation_json, region_name
|
|
|
|
)
|
|
|
|
return cls.create_from_cloudformation_json(
|
|
|
|
new_resource_name, cloudformation_json, region_name
|
|
|
|
)
|
2016-04-28 13:21:54 +00:00
|
|
|
|
|
|
|
@classmethod
|
2019-10-31 15:44:26 +00:00
|
|
|
def delete_from_cloudformation_json(
|
|
|
|
cls, resource_name, cloudformation_json, region_name
|
|
|
|
):
|
2017-02-24 02:37:43 +00:00
|
|
|
# this will break if you changed the zone the record is in,
|
|
|
|
# unfortunately
|
2019-10-31 15:44:26 +00:00
|
|
|
properties = cloudformation_json["Properties"]
|
2016-04-28 13:21:54 +00:00
|
|
|
|
|
|
|
zone_name = properties.get("HostedZoneName")
|
|
|
|
if zone_name:
|
|
|
|
hosted_zone = route53_backend.get_hosted_zone_by_name(zone_name)
|
|
|
|
else:
|
2019-10-31 15:44:26 +00:00
|
|
|
hosted_zone = route53_backend.get_hosted_zone(properties["HostedZoneId"])
|
2016-04-28 13:21:54 +00:00
|
|
|
|
|
|
|
try:
|
2019-10-31 15:44:26 +00:00
|
|
|
hosted_zone.delete_rrset({"Name": resource_name})
|
2016-04-28 13:21:54 +00:00
|
|
|
except KeyError:
|
|
|
|
pass
|
|
|
|
|
2016-04-28 13:42:10 +00:00
|
|
|
@property
|
|
|
|
def physical_resource_id(self):
|
|
|
|
return self.name
|
|
|
|
|
2015-01-17 19:50:19 +00:00
|
|
|
def to_xml(self):
|
2019-10-31 15:44:26 +00:00
|
|
|
template = Template(
|
|
|
|
"""<ResourceRecordSet>
|
2015-01-17 19:50:19 +00:00
|
|
|
<Name>{{ record_set.name }}</Name>
|
2019-02-27 10:54:55 +00:00
|
|
|
<Type>{{ record_set.type_ }}</Type>
|
2015-01-17 20:37:46 +00:00
|
|
|
{% if record_set.set_identifier %}
|
|
|
|
<SetIdentifier>{{ record_set.set_identifier }}</SetIdentifier>
|
|
|
|
{% endif %}
|
|
|
|
{% if record_set.weight %}
|
|
|
|
<Weight>{{ record_set.weight }}</Weight>
|
|
|
|
{% endif %}
|
2015-09-08 21:36:32 +00:00
|
|
|
{% if record_set.region %}
|
|
|
|
<Region>{{ record_set.region }}</Region>
|
|
|
|
{% endif %}
|
2018-03-29 14:08:39 +00:00
|
|
|
{% if record_set.ttl %}
|
|
|
|
<TTL>{{ record_set.ttl }}</TTL>
|
|
|
|
{% endif %}
|
2020-03-02 12:46:15 +00:00
|
|
|
{% if record_set.failover %}
|
|
|
|
<Failover>{{ record_set.failover }}</Failover>
|
|
|
|
{% endif %}
|
|
|
|
{% if record_set.geo_location %}
|
|
|
|
<GeoLocation>
|
|
|
|
{% for geo_key in ['ContinentCode','CountryCode','SubdivisionCode'] %}
|
|
|
|
{% if record_set.geo_location[geo_key] %}<{{ geo_key }}>{{ record_set.geo_location[geo_key] }}</{{ geo_key }}>{% endif %}
|
|
|
|
{% endfor %}
|
|
|
|
</GeoLocation>
|
|
|
|
{% endif %}
|
2018-06-22 02:09:04 +00:00
|
|
|
{% if record_set.alias_target %}
|
|
|
|
<AliasTarget>
|
|
|
|
<HostedZoneId>{{ record_set.alias_target['HostedZoneId'] }}</HostedZoneId>
|
|
|
|
<DNSName>{{ record_set.alias_target['DNSName'] }}</DNSName>
|
|
|
|
<EvaluateTargetHealth>{{ record_set.alias_target['EvaluateTargetHealth'] }}</EvaluateTargetHealth>
|
|
|
|
</AliasTarget>
|
|
|
|
{% else %}
|
2015-01-17 19:50:19 +00:00
|
|
|
<ResourceRecords>
|
|
|
|
{% for record in record_set.records %}
|
|
|
|
<ResourceRecord>
|
2020-11-18 07:23:49 +00:00
|
|
|
<Value>{{ record|e }}</Value>
|
2015-01-17 19:50:19 +00:00
|
|
|
</ResourceRecord>
|
|
|
|
{% endfor %}
|
|
|
|
</ResourceRecords>
|
2018-06-22 02:09:04 +00:00
|
|
|
{% endif %}
|
2015-01-18 00:06:43 +00:00
|
|
|
{% if record_set.health_check %}
|
|
|
|
<HealthCheckId>{{ record_set.health_check }}</HealthCheckId>
|
|
|
|
{% endif %}
|
2019-10-31 15:44:26 +00:00
|
|
|
</ResourceRecordSet>"""
|
|
|
|
)
|
2015-01-17 19:50:19 +00:00
|
|
|
return template.render(record_set=self)
|
|
|
|
|
2016-03-02 22:33:02 +00:00
|
|
|
def delete(self, *args, **kwargs):
|
2021-06-23 15:57:09 +00:00
|
|
|
"""Not exposed as part of the Route 53 API - used for CloudFormation. args are ignored"""
|
2019-10-31 15:44:26 +00:00
|
|
|
hosted_zone = route53_backend.get_hosted_zone_by_name(self.hosted_zone_name)
|
2016-03-07 19:25:25 +00:00
|
|
|
if not hosted_zone:
|
|
|
|
hosted_zone = route53_backend.get_hosted_zone(self.hosted_zone_id)
|
2019-10-31 15:44:26 +00:00
|
|
|
hosted_zone.delete_rrset({"Name": self.name, "Type": self.type_})
|
2016-03-02 22:33:02 +00:00
|
|
|
|
2015-01-17 19:50:19 +00:00
|
|
|
|
2019-06-14 14:17:50 +00:00
|
|
|
def reverse_domain_name(domain_name):
|
2019-10-31 15:44:26 +00:00
|
|
|
if domain_name.endswith("."): # normalize without trailing dot
|
2019-06-14 14:17:50 +00:00
|
|
|
domain_name = domain_name[:-1]
|
2019-10-31 15:44:26 +00:00
|
|
|
return ".".join(reversed(domain_name.split(".")))
|
2016-03-02 22:33:02 +00:00
|
|
|
|
2015-01-17 19:50:19 +00:00
|
|
|
|
2020-08-01 14:23:36 +00:00
|
|
|
class FakeZone(CloudFormationModel):
|
2016-06-03 13:57:33 +00:00
|
|
|
def __init__(self, name, id_, private_zone, comment=None):
|
2013-11-14 19:14:14 +00:00
|
|
|
self.name = name
|
2014-08-30 01:14:24 +00:00
|
|
|
self.id = id_
|
2016-02-17 14:32:38 +00:00
|
|
|
if comment is not None:
|
|
|
|
self.comment = comment
|
2016-06-03 13:57:33 +00:00
|
|
|
self.private_zone = private_zone
|
2015-01-17 15:17:25 +00:00
|
|
|
self.rrsets = []
|
2013-11-14 19:14:14 +00:00
|
|
|
|
2015-01-17 19:50:19 +00:00
|
|
|
def add_rrset(self, record_set):
|
|
|
|
record_set = RecordSet(record_set)
|
|
|
|
self.rrsets.append(record_set)
|
2015-01-17 20:37:46 +00:00
|
|
|
return record_set
|
2013-11-14 19:14:14 +00:00
|
|
|
|
2016-04-12 17:37:01 +00:00
|
|
|
def upsert_rrset(self, record_set):
|
|
|
|
new_rrset = RecordSet(record_set)
|
|
|
|
for i, rrset in enumerate(self.rrsets):
|
2019-10-31 15:44:26 +00:00
|
|
|
if (
|
|
|
|
rrset.name == new_rrset.name
|
|
|
|
and rrset.type_ == new_rrset.type_
|
|
|
|
and rrset.set_identifier == new_rrset.set_identifier
|
|
|
|
):
|
2016-04-12 17:37:01 +00:00
|
|
|
self.rrsets[i] = new_rrset
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
self.rrsets.append(new_rrset)
|
|
|
|
return new_rrset
|
|
|
|
|
2019-06-17 13:53:32 +00:00
|
|
|
def delete_rrset(self, rrset):
|
2017-02-24 02:37:43 +00:00
|
|
|
self.rrsets = [
|
2019-06-17 13:53:32 +00:00
|
|
|
record_set
|
|
|
|
for record_set in self.rrsets
|
2019-10-31 15:44:26 +00:00
|
|
|
if record_set.name != rrset["Name"]
|
|
|
|
or (rrset.get("Type") is not None and record_set.type_ != rrset["Type"])
|
2019-06-17 13:53:32 +00:00
|
|
|
]
|
2015-01-17 19:50:19 +00:00
|
|
|
|
2015-04-30 22:51:01 +00:00
|
|
|
def delete_rrset_by_id(self, set_identifier):
|
2017-02-24 02:37:43 +00:00
|
|
|
self.rrsets = [
|
2019-10-31 15:44:26 +00:00
|
|
|
record_set
|
|
|
|
for record_set in self.rrsets
|
|
|
|
if record_set.set_identifier != set_identifier
|
|
|
|
]
|
2015-04-30 22:51:01 +00:00
|
|
|
|
2017-10-23 15:25:40 +00:00
|
|
|
def get_record_sets(self, start_type, start_name):
|
2021-04-20 11:50:05 +00:00
|
|
|
def predicate(rrset):
|
|
|
|
rrset_name_reversed = reverse_domain_name(rrset.name)
|
|
|
|
start_name_reversed = reverse_domain_name(start_name)
|
|
|
|
return rrset_name_reversed < start_name_reversed or (
|
|
|
|
rrset_name_reversed == start_name_reversed and rrset.type_ < start_type
|
|
|
|
)
|
|
|
|
|
|
|
|
record_sets = sorted(
|
|
|
|
self.rrsets,
|
|
|
|
key=lambda rrset: (reverse_domain_name(rrset.name), rrset.type_),
|
|
|
|
)
|
|
|
|
|
2017-10-23 15:25:40 +00:00
|
|
|
if start_name:
|
2021-04-20 11:50:05 +00:00
|
|
|
start_type = start_type or ""
|
|
|
|
record_sets = itertools.dropwhile(predicate, record_sets)
|
2015-01-17 19:50:19 +00:00
|
|
|
|
|
|
|
return record_sets
|
2015-01-17 15:17:25 +00:00
|
|
|
|
|
|
|
@property
|
|
|
|
def physical_resource_id(self):
|
2017-10-21 21:10:45 +00:00
|
|
|
return self.id
|
2015-01-17 15:17:25 +00:00
|
|
|
|
2020-08-01 14:23:36 +00:00
|
|
|
@staticmethod
|
|
|
|
def cloudformation_name_type():
|
|
|
|
return "Name"
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def cloudformation_type():
|
|
|
|
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-route53-hostedzone.html
|
|
|
|
return "AWS::Route53::HostedZone"
|
|
|
|
|
2015-01-17 15:17:25 +00:00
|
|
|
@classmethod
|
2019-10-31 15:44:26 +00:00
|
|
|
def create_from_cloudformation_json(
|
2021-11-03 21:00:42 +00:00
|
|
|
cls, resource_name, cloudformation_json, region_name, **kwargs
|
2019-10-31 15:44:26 +00:00
|
|
|
):
|
2020-08-27 09:11:47 +00:00
|
|
|
hosted_zone = route53_backend.create_hosted_zone(
|
|
|
|
resource_name, private_zone=False
|
|
|
|
)
|
2015-01-17 15:17:25 +00:00
|
|
|
return hosted_zone
|
|
|
|
|
|
|
|
|
2020-08-01 14:23:36 +00:00
|
|
|
class RecordSetGroup(CloudFormationModel):
|
2015-01-17 19:50:19 +00:00
|
|
|
def __init__(self, hosted_zone_id, record_sets):
|
|
|
|
self.hosted_zone_id = hosted_zone_id
|
2015-01-17 15:17:25 +00:00
|
|
|
self.record_sets = record_sets
|
|
|
|
|
2015-01-17 19:50:19 +00:00
|
|
|
@property
|
|
|
|
def physical_resource_id(self):
|
2021-10-22 21:47:29 +00:00
|
|
|
return f"arn:aws:route53:::hostedzone/{self.hosted_zone_id}"
|
2015-01-17 19:50:19 +00:00
|
|
|
|
2020-08-01 14:23:36 +00:00
|
|
|
@staticmethod
|
|
|
|
def cloudformation_name_type():
|
|
|
|
return None
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def cloudformation_type():
|
|
|
|
# https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-route53-recordsetgroup.html
|
|
|
|
return "AWS::Route53::RecordSetGroup"
|
|
|
|
|
2015-01-17 15:17:25 +00:00
|
|
|
@classmethod
|
2019-10-31 15:44:26 +00:00
|
|
|
def create_from_cloudformation_json(
|
2021-11-03 21:00:42 +00:00
|
|
|
cls, resource_name, cloudformation_json, region_name, **kwargs
|
2019-10-31 15:44:26 +00:00
|
|
|
):
|
|
|
|
properties = cloudformation_json["Properties"]
|
2015-01-17 15:17:25 +00:00
|
|
|
|
2017-03-05 03:31:45 +00:00
|
|
|
zone_name = properties.get("HostedZoneName")
|
|
|
|
if zone_name:
|
|
|
|
hosted_zone = route53_backend.get_hosted_zone_by_name(zone_name)
|
|
|
|
else:
|
|
|
|
hosted_zone = route53_backend.get_hosted_zone(properties["HostedZoneId"])
|
2015-01-17 15:17:25 +00:00
|
|
|
record_sets = properties["RecordSets"]
|
|
|
|
for record_set in record_sets:
|
2015-01-17 19:50:19 +00:00
|
|
|
hosted_zone.add_rrset(record_set)
|
2015-01-17 15:17:25 +00:00
|
|
|
|
2015-01-17 19:50:19 +00:00
|
|
|
record_set_group = RecordSetGroup(hosted_zone.id, record_sets)
|
2015-01-17 15:17:25 +00:00
|
|
|
return record_set_group
|
2013-11-14 19:14:14 +00:00
|
|
|
|
|
|
|
|
2021-10-22 21:47:29 +00:00
|
|
|
class QueryLoggingConfig(BaseModel):
|
|
|
|
|
|
|
|
"""QueryLoggingConfig class; this object isn't part of Cloudformation."""
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
self, query_logging_config_id, hosted_zone_id, cloudwatch_logs_log_group_arn
|
|
|
|
):
|
|
|
|
self.hosted_zone_id = hosted_zone_id
|
|
|
|
self.cloudwatch_logs_log_group_arn = cloudwatch_logs_log_group_arn
|
|
|
|
self.query_logging_config_id = query_logging_config_id
|
|
|
|
self.location = f"https://route53.amazonaws.com/2013-04-01/queryloggingconfig/{self.query_logging_config_id}"
|
|
|
|
|
|
|
|
def to_xml(self):
|
|
|
|
template = Template(
|
|
|
|
"""<QueryLoggingConfig>
|
|
|
|
<CloudWatchLogsLogGroupArn>{{ query_logging_config.cloudwatch_logs_log_group_arn }}</CloudWatchLogsLogGroupArn>
|
|
|
|
<HostedZoneId>{{ query_logging_config.hosted_zone_id }}</HostedZoneId>
|
|
|
|
<Id>{{ query_logging_config.query_logging_config_id }}</Id>
|
|
|
|
</QueryLoggingConfig>"""
|
|
|
|
)
|
|
|
|
# The "Location" value must be put into the header; that's done in
|
|
|
|
# responses.py.
|
|
|
|
return template.render(query_logging_config=self)
|
|
|
|
|
|
|
|
|
2013-11-14 19:14:14 +00:00
|
|
|
class Route53Backend(BaseBackend):
|
|
|
|
def __init__(self):
|
|
|
|
self.zones = {}
|
2015-01-18 00:06:43 +00:00
|
|
|
self.health_checks = {}
|
2016-09-21 00:41:23 +00:00
|
|
|
self.resource_tags = defaultdict(dict)
|
2021-10-22 21:47:29 +00:00
|
|
|
self.query_logging_configs = {}
|
2013-11-14 19:14:14 +00:00
|
|
|
|
2016-06-03 13:57:33 +00:00
|
|
|
def create_hosted_zone(self, name, private_zone, comment=None):
|
2017-09-22 22:50:01 +00:00
|
|
|
new_id = create_route53_zone_id()
|
2019-10-31 15:44:26 +00:00
|
|
|
new_zone = FakeZone(name, new_id, private_zone=private_zone, comment=comment)
|
2013-11-14 19:14:14 +00:00
|
|
|
self.zones[new_id] = new_zone
|
|
|
|
return new_zone
|
|
|
|
|
2016-09-21 00:41:23 +00:00
|
|
|
def change_tags_for_resource(self, resource_id, tags):
|
2019-10-31 15:44:26 +00:00
|
|
|
if "Tag" in tags:
|
|
|
|
if isinstance(tags["Tag"], list):
|
|
|
|
for tag in tags["Tag"]:
|
|
|
|
self.resource_tags[resource_id][tag["Key"]] = tag["Value"]
|
2016-09-23 01:38:47 +00:00
|
|
|
else:
|
2019-10-31 15:44:26 +00:00
|
|
|
key, value = (tags["Tag"]["Key"], tags["Tag"]["Value"])
|
2016-09-23 01:38:47 +00:00
|
|
|
self.resource_tags[resource_id][key] = value
|
2016-09-21 00:41:23 +00:00
|
|
|
else:
|
2019-10-31 15:44:26 +00:00
|
|
|
if "Key" in tags:
|
|
|
|
if isinstance(tags["Key"], list):
|
|
|
|
for key in tags["Key"]:
|
|
|
|
del self.resource_tags[resource_id][key]
|
2016-09-21 00:41:23 +00:00
|
|
|
else:
|
2019-10-31 15:44:26 +00:00
|
|
|
del self.resource_tags[resource_id][tags["Key"]]
|
2016-09-21 00:41:23 +00:00
|
|
|
|
|
|
|
def list_tags_for_resource(self, resource_id):
|
|
|
|
if resource_id in self.resource_tags:
|
|
|
|
return self.resource_tags[resource_id]
|
2019-08-28 18:55:19 +00:00
|
|
|
return {}
|
2016-09-21 00:41:23 +00:00
|
|
|
|
2021-10-12 22:04:43 +00:00
|
|
|
def change_resource_record_sets(self, the_zone, change_list):
|
|
|
|
for value in change_list:
|
|
|
|
action = value["Action"]
|
|
|
|
record_set = value["ResourceRecordSet"]
|
|
|
|
|
|
|
|
cleaned_record_name = record_set["Name"].strip(".")
|
|
|
|
cleaned_hosted_zone_name = the_zone.name.strip(".")
|
|
|
|
|
|
|
|
if not cleaned_record_name.endswith(cleaned_hosted_zone_name):
|
2021-10-22 21:47:29 +00:00
|
|
|
error_msg = f"""
|
2021-10-12 22:04:43 +00:00
|
|
|
An error occurred (InvalidChangeBatch) when calling the ChangeResourceRecordSets operation:
|
2021-10-22 21:47:29 +00:00
|
|
|
RRSet with DNS name {record_set["Name"]} is not permitted in zone {the_zone.name}
|
|
|
|
"""
|
2021-10-12 22:04:43 +00:00
|
|
|
return error_msg
|
|
|
|
|
|
|
|
if not record_set["Name"].endswith("."):
|
|
|
|
record_set["Name"] += "."
|
|
|
|
|
|
|
|
if action in ("CREATE", "UPSERT"):
|
|
|
|
if "ResourceRecords" in record_set:
|
|
|
|
resource_records = list(record_set["ResourceRecords"].values())[0]
|
|
|
|
if not isinstance(resource_records, list):
|
|
|
|
# Depending on how many records there are, this may
|
|
|
|
# or may not be a list
|
|
|
|
resource_records = [resource_records]
|
|
|
|
record_set["ResourceRecords"] = [
|
|
|
|
x["Value"] for x in resource_records
|
|
|
|
]
|
|
|
|
if action == "CREATE":
|
|
|
|
the_zone.add_rrset(record_set)
|
|
|
|
else:
|
|
|
|
the_zone.upsert_rrset(record_set)
|
|
|
|
elif action == "DELETE":
|
|
|
|
if "SetIdentifier" in record_set:
|
|
|
|
the_zone.delete_rrset_by_id(record_set["SetIdentifier"])
|
|
|
|
else:
|
|
|
|
the_zone.delete_rrset(record_set)
|
|
|
|
return None
|
|
|
|
|
|
|
|
def list_hosted_zones(self):
|
2013-11-14 19:14:14 +00:00
|
|
|
return self.zones.values()
|
|
|
|
|
2021-10-12 22:04:43 +00:00
|
|
|
def list_hosted_zones_by_name(self, dnsname):
|
|
|
|
if dnsname:
|
|
|
|
dnsname = dnsname[0]
|
|
|
|
if dnsname[-1] != ".":
|
|
|
|
dnsname += "."
|
|
|
|
zones = [zone for zone in self.list_hosted_zones() if zone.name == dnsname]
|
|
|
|
else:
|
|
|
|
# sort by names, but with domain components reversed
|
|
|
|
# see http://boto3.readthedocs.io/en/latest/reference/services/route53.html#Route53.Client.list_hosted_zones_by_name
|
|
|
|
|
|
|
|
def sort_key(zone):
|
|
|
|
domains = zone.name.split(".")
|
|
|
|
if domains[-1] == "":
|
|
|
|
domains = domains[-1:] + domains[:-1]
|
|
|
|
return ".".join(reversed(domains))
|
|
|
|
|
|
|
|
zones = self.list_hosted_zones()
|
|
|
|
zones = sorted(zones, key=sort_key)
|
|
|
|
return dnsname, zones
|
|
|
|
|
2014-08-30 01:14:24 +00:00
|
|
|
def get_hosted_zone(self, id_):
|
2017-03-17 02:45:58 +00:00
|
|
|
return self.zones.get(id_.replace("/hostedzone/", ""))
|
2013-11-14 19:14:14 +00:00
|
|
|
|
2015-01-17 15:17:25 +00:00
|
|
|
def get_hosted_zone_by_name(self, name):
|
2021-10-12 22:04:43 +00:00
|
|
|
for zone in self.list_hosted_zones():
|
2015-01-17 15:17:25 +00:00
|
|
|
if zone.name == name:
|
|
|
|
return zone
|
2021-10-22 21:47:29 +00:00
|
|
|
return None
|
2015-01-17 15:17:25 +00:00
|
|
|
|
2014-08-30 01:14:24 +00:00
|
|
|
def delete_hosted_zone(self, id_):
|
2017-03-17 02:45:58 +00:00
|
|
|
return self.zones.pop(id_.replace("/hostedzone/", ""), None)
|
2013-11-14 19:14:14 +00:00
|
|
|
|
2021-08-22 09:51:21 +00:00
|
|
|
def create_health_check(self, caller_reference, health_check_args):
|
2015-01-18 00:06:43 +00:00
|
|
|
health_check_id = str(uuid.uuid4())
|
2021-08-22 09:51:21 +00:00
|
|
|
health_check = HealthCheck(health_check_id, caller_reference, health_check_args)
|
2015-01-18 00:06:43 +00:00
|
|
|
self.health_checks[health_check_id] = health_check
|
|
|
|
return health_check
|
|
|
|
|
2021-10-12 22:04:43 +00:00
|
|
|
def list_health_checks(self):
|
2015-01-18 00:06:43 +00:00
|
|
|
return self.health_checks.values()
|
|
|
|
|
|
|
|
def delete_health_check(self, health_check_id):
|
|
|
|
return self.health_checks.pop(health_check_id, None)
|
2013-11-14 19:14:14 +00:00
|
|
|
|
2021-10-22 21:47:29 +00:00
|
|
|
@staticmethod
|
|
|
|
def _validate_arn(region, arn):
|
|
|
|
match = re.match(fr"arn:aws:logs:{region}:\d{{12}}:log-group:.+", arn)
|
|
|
|
if not arn or not match:
|
|
|
|
raise InvalidInput()
|
|
|
|
|
|
|
|
# The CloudWatch Logs log group must be in the "us-east-1" region.
|
|
|
|
match = re.match(r"^(?:[^:]+:){3}(?P<region>[^:]+).*", arn)
|
|
|
|
if match.group("region") != "us-east-1":
|
|
|
|
raise InvalidInput()
|
|
|
|
|
|
|
|
def create_query_logging_config(self, region, hosted_zone_id, log_group_arn):
|
|
|
|
"""Process the create_query_logging_config request."""
|
|
|
|
# Does the hosted_zone_id exist?
|
|
|
|
response = self.list_hosted_zones()
|
|
|
|
zones = list(response) if response else []
|
|
|
|
for zone in zones:
|
|
|
|
if zone.id == hosted_zone_id:
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
raise NoSuchHostedZone(hosted_zone_id)
|
|
|
|
|
|
|
|
# Ensure CloudWatch Logs log ARN is valid, otherwise raise an error.
|
|
|
|
self._validate_arn(region, log_group_arn)
|
|
|
|
|
|
|
|
# Note: boto3 checks the resource policy permissions before checking
|
|
|
|
# whether the log group exists. moto doesn't have a way of checking
|
|
|
|
# the resource policy, so in some instances moto will complain
|
|
|
|
# about a log group that doesn't exist whereas boto3 will complain
|
|
|
|
# that "The resource policy that you're using for Route 53 query
|
|
|
|
# logging doesn't grant Route 53 sufficient permission to create
|
|
|
|
# a log stream in the specified log group."
|
|
|
|
|
|
|
|
from moto.logs import logs_backends # pylint: disable=import-outside-toplevel
|
|
|
|
|
|
|
|
response = logs_backends[region].describe_log_groups()
|
|
|
|
log_groups = response[0] if response else []
|
|
|
|
for entry in log_groups:
|
|
|
|
if log_group_arn == entry["arn"]:
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
# There is no CloudWatch Logs log group with the specified ARN.
|
|
|
|
raise NoSuchCloudWatchLogsLogGroup()
|
|
|
|
|
|
|
|
# Verify there is no existing query log config using the same hosted
|
|
|
|
# zone.
|
|
|
|
for query_log in self.query_logging_configs.values():
|
|
|
|
if query_log.hosted_zone_id == hosted_zone_id:
|
|
|
|
raise QueryLoggingConfigAlreadyExists()
|
|
|
|
|
|
|
|
# Create an instance of the query logging config.
|
|
|
|
query_logging_config_id = str(uuid.uuid4())
|
|
|
|
query_logging_config = QueryLoggingConfig(
|
|
|
|
query_logging_config_id, hosted_zone_id, log_group_arn
|
|
|
|
)
|
|
|
|
self.query_logging_configs[query_logging_config_id] = query_logging_config
|
|
|
|
return query_logging_config
|
|
|
|
|
|
|
|
def delete_query_logging_config(self, query_logging_config_id):
|
|
|
|
"""Delete query logging config, if it exists."""
|
|
|
|
if query_logging_config_id not in self.query_logging_configs:
|
|
|
|
raise NoSuchQueryLoggingConfig()
|
|
|
|
self.query_logging_configs.pop(query_logging_config_id)
|
|
|
|
|
|
|
|
def get_query_logging_config(self, query_logging_config_id):
|
|
|
|
"""Return query logging config, if it exists."""
|
|
|
|
if query_logging_config_id not in self.query_logging_configs:
|
|
|
|
raise NoSuchQueryLoggingConfig()
|
|
|
|
return self.query_logging_configs[query_logging_config_id]
|
|
|
|
|
|
|
|
@paginate(pagination_model=PAGINATION_MODEL)
|
|
|
|
def list_query_logging_configs(
|
|
|
|
self, hosted_zone_id=None, next_token=None, max_results=None,
|
|
|
|
): # pylint: disable=unused-argument
|
|
|
|
"""Return a list of query logging configs."""
|
|
|
|
if hosted_zone_id:
|
|
|
|
# Does the hosted_zone_id exist?
|
|
|
|
response = self.list_hosted_zones()
|
|
|
|
zones = list(response) if response else []
|
|
|
|
for zone in zones:
|
|
|
|
if zone.id == hosted_zone_id:
|
|
|
|
break
|
|
|
|
else:
|
|
|
|
raise NoSuchHostedZone(hosted_zone_id)
|
|
|
|
|
|
|
|
return list(self.query_logging_configs.values())
|
|
|
|
|
2017-02-24 02:37:43 +00:00
|
|
|
|
2013-11-14 19:14:14 +00:00
|
|
|
route53_backend = Route53Backend()
|