* fix OPTIONS requests on non-existing API GW integrations * add cloudformation models for API Gateway deployments * bump version * add backdoor to return CloudWatch metrics * Updating implementation coverage * Updating implementation coverage * add cloudformation models for API Gateway deployments * Updating implementation coverage * Updating implementation coverage * Implemented get-caller-identity returning real data depending on the access key used. * bump version * minor fixes * fix Number data_type for SQS message attribute * fix handling of encoding errors * bump version * make CF stack queryable before starting to initialize its resources * bump version * fix integration_method for API GW method integrations * fix undefined status in CF FakeStack * Fix apigateway issues with terraform v0.12.21 * resource_methods -> add handle for "DELETE" method * integrations -> fix issue that "httpMethod" wasn't included in body request (this value was set as the value from refer method resource) * bump version * Fix setting http method for API gateway integrations (#6) * bump version * remove duplicate methods * add storage class to S3 Key when completing multipart upload (#7) * fix SQS performance issues; bump version * add pagination to SecretsManager list-secrets (#9) * fix default parameter groups in RDS * fix adding S3 metadata headers with names containing dots (#13) * Updating implementation coverage * Updating implementation coverage * add cloudformation models for API Gateway deployments * Updating implementation coverage * Updating implementation coverage * Implemented get-caller-identity returning real data depending on the access key used. * make CF stack queryable before starting to initialize its resources * bump version * remove duplicate methods * fix adding S3 metadata headers with names containing dots (#13) * Update amis.json to support EKS AMI mocks (#15) * fix PascalCase for boolean value in ListMultipartUploads response (#17); fix _get_multi_param to parse nested list/dict query params * determine non-zero container exit code in Batch API * support filtering by dimensions in CW get_metric_statistics * fix storing attributes for ELBv2 Route entities; API GW refactorings for TF tests * add missing fields for API GW resources * fix error messages for Route53 (TF-compat) * various fixes for IAM resources (tf-compat) * minor fixes for API GW models (tf-compat) * minor fixes for API GW responses (tf-compat) * add s3 exception for bucket notification filter rule validation * change the way RESTErrors generate the response body and content-type header * fix lint errors and disable "black" syntax enforcement * remove return type hint in RESTError.get_body * add RESTError XML template for IAM exceptions * add support for API GW minimumCompressionSize * fix casing getting PrivateDnsEnabled API GW attribute * minor fixes for error responses * fix escaping special chars for IAM role descriptions (tf-compat) * minor fixes and tagging support for API GW and ELB v2 (tf-compat) * Merge branch 'master' into localstack * add "AlarmRule" attribute to enable support for composite CloudWatch metrics * fix recursive parsing of complex/nested query params * bump version * add API to delete S3 website configurations (#18) * use dict copy to allow parallelism and avoid concurrent modification exceptions in S3 * fix precondition check for etags in S3 (#19) * minor fix for user filtering in Cognito * fix API Gateway error response; avoid returning empty response templates (tf-compat) * support tags and tracingEnabled attribute for API GW stages * fix boolean value in S3 encryption response (#20) * fix connection arn structure * fix api destination arn structure * black format * release 2.0.3.37 * fix s3 exception tests see botocore/parsers.py:1002 where RequestId is removed from parsed * remove python 2 from build action * add test failure annotations in build action * fix events test arn comparisons * fix s3 encryption response test * return default value "0" if EC2 availableIpAddressCount is empty * fix extracting SecurityGroupIds for EC2 VPC endpoints * support deleting/updating API Gateway DomainNames * fix(events): Return empty string instead of null when no pattern is specified in EventPattern (tf-compat) (#22) * fix logic and revert CF changes to get tests running again (#21) * add support for EC2 customer gateway API (#25) * add support for EC2 Transit Gateway APIs (#24) * feat(logs): add `kmsKeyId` into `LogGroup` entity (#23) * minor change in ELBv2 logic to fix tests * feat(events): add APIs to describe and delete CloudWatch Events connections (#26) * add support for EC2 transit gateway route tables (#27) * pass transit gateway route table ID in Describe API, minor refactoring (#29) * add support for EC2 Transit Gateway Routes (#28) * fix region on ACM certificate import (#31) * add support for EC2 transit gateway attachments (#30) * add support for EC2 Transit Gateway VPN attachments (#32) * fix account ID for logs API * add support for DeleteOrganization API * feat(events): store raw filter representation for CloudWatch events patterns (tf-compat) (#36) * feat(events): add support to describe/update/delete CloudWatch API destinations (#35) * add Cognito UpdateIdentityPool, CW Logs PutResourcePolicy * feat(events): add support for tags in EventBus API (#38) * fix parameter validation for Batch compute environments (tf-compat) * revert merge conflicts in IMPLEMENTATION_COVERAGE.md * format code using black * restore original README; re-enable and fix CloudFormation tests * restore tests and old logic for CF stack parameters from SSM * parameterize RequestId/RequestID in response messages and revert related test changes * undo LocalStack-specific adaptations * minor fix * Update CodeCov config to reflect removal of Py2 * undo change related to CW metric filtering; add additional test for CW metric statistics with dimensions * Terraform - Extend whitelist of running tests Co-authored-by: acsbendi <acsbendi28@gmail.com> Co-authored-by: Phan Duong <duongpv@outlook.com> Co-authored-by: Thomas Rausch <thomas@thrau.at> Co-authored-by: Macwan Nevil <macnev2013@gmail.com> Co-authored-by: Dominik Schubert <dominik.schubert91@gmail.com> Co-authored-by: Gonzalo Saad <saad.gonzalo.ale@gmail.com> Co-authored-by: Mohit Alonja <monty16597@users.noreply.github.com> Co-authored-by: Miguel Gagliardo <migag9@gmail.com> Co-authored-by: Bert Blommers <info@bertblommers.nl>
425 lines
16 KiB
Python
425 lines
16 KiB
Python
from __future__ import unicode_literals
|
|
from jinja2 import Template
|
|
from urllib.parse import parse_qs, urlparse
|
|
|
|
from moto.core.responses import BaseResponse
|
|
from .models import route53_backend
|
|
import xmltodict
|
|
|
|
XMLNS = "https://route53.amazonaws.com/doc/2013-04-01/"
|
|
|
|
|
|
class Route53(BaseResponse):
|
|
def list_or_create_hostzone_response(self, request, full_url, headers):
|
|
self.setup_class(request, full_url, headers)
|
|
|
|
if request.method == "POST":
|
|
elements = xmltodict.parse(self.body)
|
|
if "HostedZoneConfig" in elements["CreateHostedZoneRequest"]:
|
|
comment = elements["CreateHostedZoneRequest"]["HostedZoneConfig"][
|
|
"Comment"
|
|
]
|
|
try:
|
|
# in boto3, this field is set directly in the xml
|
|
private_zone = elements["CreateHostedZoneRequest"][
|
|
"HostedZoneConfig"
|
|
]["PrivateZone"]
|
|
except KeyError:
|
|
# if a VPC subsection is only included in xmls params when private_zone=True,
|
|
# see boto: boto/route53/connection.py
|
|
private_zone = "VPC" in elements["CreateHostedZoneRequest"]
|
|
else:
|
|
comment = None
|
|
private_zone = False
|
|
|
|
name = elements["CreateHostedZoneRequest"]["Name"]
|
|
|
|
if name[-1] != ".":
|
|
name += "."
|
|
|
|
new_zone = route53_backend.create_hosted_zone(
|
|
name, comment=comment, private_zone=private_zone
|
|
)
|
|
template = Template(CREATE_HOSTED_ZONE_RESPONSE)
|
|
return 201, headers, template.render(zone=new_zone)
|
|
|
|
elif request.method == "GET":
|
|
all_zones = route53_backend.get_all_hosted_zones()
|
|
template = Template(LIST_HOSTED_ZONES_RESPONSE)
|
|
return 200, headers, template.render(zones=all_zones)
|
|
|
|
def list_hosted_zones_by_name_response(self, request, full_url, headers):
|
|
self.setup_class(request, full_url, headers)
|
|
parsed_url = urlparse(full_url)
|
|
query_params = parse_qs(parsed_url.query)
|
|
dnsname = query_params.get("dnsname")
|
|
|
|
if dnsname:
|
|
dnsname = dnsname[0]
|
|
if dnsname[-1] != ".":
|
|
dnsname += "."
|
|
zones = [
|
|
zone
|
|
for zone in route53_backend.get_all_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 = route53_backend.get_all_hosted_zones()
|
|
zones = sorted(zones, key=sort_key)
|
|
|
|
template = Template(LIST_HOSTED_ZONES_BY_NAME_RESPONSE)
|
|
return 200, headers, template.render(zones=zones, dnsname=dnsname)
|
|
|
|
def get_or_delete_hostzone_response(self, request, full_url, headers):
|
|
self.setup_class(request, full_url, headers)
|
|
parsed_url = urlparse(full_url)
|
|
zoneid = parsed_url.path.rstrip("/").rsplit("/", 1)[1]
|
|
the_zone = route53_backend.get_hosted_zone(zoneid)
|
|
if not the_zone:
|
|
return no_such_hosted_zone_error(zoneid, headers)
|
|
|
|
if request.method == "GET":
|
|
template = Template(GET_HOSTED_ZONE_RESPONSE)
|
|
|
|
return 200, headers, template.render(zone=the_zone)
|
|
elif request.method == "DELETE":
|
|
route53_backend.delete_hosted_zone(zoneid)
|
|
return 200, headers, DELETE_HOSTED_ZONE_RESPONSE
|
|
|
|
def rrset_response(self, request, full_url, headers):
|
|
self.setup_class(request, full_url, headers)
|
|
|
|
parsed_url = urlparse(full_url)
|
|
method = request.method
|
|
|
|
zoneid = parsed_url.path.rstrip("/").rsplit("/", 2)[1]
|
|
the_zone = route53_backend.get_hosted_zone(zoneid)
|
|
if not the_zone:
|
|
return no_such_hosted_zone_error(zoneid, headers)
|
|
|
|
if method == "POST":
|
|
elements = xmltodict.parse(self.body)
|
|
|
|
change_list = elements["ChangeResourceRecordSetsRequest"]["ChangeBatch"][
|
|
"Changes"
|
|
]["Change"]
|
|
if not isinstance(change_list, list):
|
|
change_list = [
|
|
elements["ChangeResourceRecordSetsRequest"]["ChangeBatch"][
|
|
"Changes"
|
|
]["Change"]
|
|
]
|
|
|
|
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):
|
|
error_msg = """
|
|
An error occurred (InvalidChangeBatch) when calling the ChangeResourceRecordSets operation:
|
|
RRSet with DNS name %s is not permitted in zone %s
|
|
""" % (
|
|
record_set["Name"],
|
|
the_zone.name,
|
|
)
|
|
return 400, headers, 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 200, headers, CHANGE_RRSET_RESPONSE
|
|
|
|
elif method == "GET":
|
|
querystring = parse_qs(parsed_url.query)
|
|
template = Template(LIST_RRSET_RESPONSE)
|
|
start_type = querystring.get("type", [None])[0]
|
|
start_name = querystring.get("name", [None])[0]
|
|
|
|
if start_type and not start_name:
|
|
return 400, headers, "The input is not valid"
|
|
|
|
record_sets = the_zone.get_record_sets(start_type, start_name)
|
|
return 200, headers, template.render(record_sets=record_sets)
|
|
|
|
def health_check_response(self, request, full_url, headers):
|
|
self.setup_class(request, full_url, headers)
|
|
|
|
parsed_url = urlparse(full_url)
|
|
method = request.method
|
|
|
|
if method == "POST":
|
|
properties = xmltodict.parse(self.body)["CreateHealthCheckRequest"][
|
|
"HealthCheckConfig"
|
|
]
|
|
health_check_args = {
|
|
"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"),
|
|
}
|
|
health_check = route53_backend.create_health_check(health_check_args)
|
|
template = Template(CREATE_HEALTH_CHECK_RESPONSE)
|
|
return 201, headers, template.render(health_check=health_check)
|
|
elif method == "DELETE":
|
|
health_check_id = parsed_url.path.split("/")[-1]
|
|
route53_backend.delete_health_check(health_check_id)
|
|
return 200, headers, DELETE_HEALTH_CHECK_RESPONSE
|
|
elif method == "GET":
|
|
template = Template(LIST_HEALTH_CHECKS_RESPONSE)
|
|
health_checks = route53_backend.get_health_checks()
|
|
return 200, headers, template.render(health_checks=health_checks)
|
|
|
|
def not_implemented_response(self, request, full_url, headers):
|
|
self.setup_class(request, full_url, headers)
|
|
|
|
action = ""
|
|
if "tags" in full_url:
|
|
action = "tags"
|
|
elif "trafficpolicyinstances" in full_url:
|
|
action = "policies"
|
|
raise NotImplementedError(
|
|
"The action for {0} has not been implemented for route 53".format(action)
|
|
)
|
|
|
|
def list_or_change_tags_for_resource_request(self, request, full_url, headers):
|
|
self.setup_class(request, full_url, headers)
|
|
|
|
parsed_url = urlparse(full_url)
|
|
id_ = parsed_url.path.split("/")[-1]
|
|
type_ = parsed_url.path.split("/")[-2]
|
|
|
|
if request.method == "GET":
|
|
tags = route53_backend.list_tags_for_resource(id_)
|
|
template = Template(LIST_TAGS_FOR_RESOURCE_RESPONSE)
|
|
return (
|
|
200,
|
|
headers,
|
|
template.render(resource_type=type_, resource_id=id_, tags=tags),
|
|
)
|
|
|
|
if request.method == "POST":
|
|
tags = xmltodict.parse(self.body)["ChangeTagsForResourceRequest"]
|
|
|
|
if "AddTags" in tags:
|
|
tags = tags["AddTags"]
|
|
elif "RemoveTagKeys" in tags:
|
|
tags = tags["RemoveTagKeys"]
|
|
|
|
route53_backend.change_tags_for_resource(id_, tags)
|
|
template = Template(CHANGE_TAGS_FOR_RESOURCE_RESPONSE)
|
|
|
|
return 200, headers, template.render()
|
|
|
|
def get_change(self, request, full_url, headers):
|
|
self.setup_class(request, full_url, headers)
|
|
|
|
if request.method == "GET":
|
|
parsed_url = urlparse(full_url)
|
|
change_id = parsed_url.path.rstrip("/").rsplit("/", 1)[1]
|
|
template = Template(GET_CHANGE_RESPONSE)
|
|
return 200, headers, template.render(change_id=change_id)
|
|
|
|
|
|
def no_such_hosted_zone_error(zoneid, headers={}):
|
|
headers["X-Amzn-ErrorType"] = "NoSuchHostedZone"
|
|
headers["Content-Type"] = "text/xml"
|
|
message = "Zone %s Not Found" % zoneid
|
|
error_response = (
|
|
"<Error><Code>NoSuchHostedZone</Code><Message>%s</Message></Error>" % message
|
|
)
|
|
error_response = '<ErrorResponse xmlns="%s">%s</ErrorResponse>' % (
|
|
XMLNS,
|
|
error_response,
|
|
)
|
|
return 404, headers, error_response
|
|
|
|
|
|
LIST_TAGS_FOR_RESOURCE_RESPONSE = """
|
|
<ListTagsForResourceResponse xmlns="https://route53.amazonaws.com/doc/2015-01-01/">
|
|
<ResourceTagSet>
|
|
<ResourceType>{{resource_type}}</ResourceType>
|
|
<ResourceId>{{resource_id}}</ResourceId>
|
|
<Tags>
|
|
{% for key, value in tags.items() %}
|
|
<Tag>
|
|
<Key>{{key}}</Key>
|
|
<Value>{{value}}</Value>
|
|
</Tag>
|
|
{% endfor %}
|
|
</Tags>
|
|
</ResourceTagSet>
|
|
</ListTagsForResourceResponse>
|
|
"""
|
|
|
|
CHANGE_TAGS_FOR_RESOURCE_RESPONSE = """<ChangeTagsForResourceResponse xmlns="https://route53.amazonaws.com/doc/2015-01-01/">
|
|
</ChangeTagsForResourceResponse>
|
|
"""
|
|
|
|
LIST_RRSET_RESPONSE = """<ListResourceRecordSetsResponse xmlns="https://route53.amazonaws.com/doc/2012-12-12/">
|
|
<ResourceRecordSets>
|
|
{% for record_set in record_sets %}
|
|
{{ record_set.to_xml() }}
|
|
{% endfor %}
|
|
</ResourceRecordSets>
|
|
<IsTruncated>false</IsTruncated>
|
|
</ListResourceRecordSetsResponse>"""
|
|
|
|
CHANGE_RRSET_RESPONSE = """<ChangeResourceRecordSetsResponse xmlns="https://route53.amazonaws.com/doc/2012-12-12/">
|
|
<ChangeInfo>
|
|
<Status>INSYNC</Status>
|
|
<SubmittedAt>2010-09-10T01:36:41.958Z</SubmittedAt>
|
|
<Id>/change/C2682N5HXP0BZ4</Id>
|
|
</ChangeInfo>
|
|
</ChangeResourceRecordSetsResponse>"""
|
|
|
|
DELETE_HOSTED_ZONE_RESPONSE = """<DeleteHostedZoneResponse xmlns="https://route53.amazonaws.com/doc/2012-12-12/">
|
|
<ChangeInfo>
|
|
</ChangeInfo>
|
|
</DeleteHostedZoneResponse>"""
|
|
|
|
GET_HOSTED_ZONE_RESPONSE = """<GetHostedZoneResponse xmlns="https://route53.amazonaws.com/doc/2012-12-12/">
|
|
<HostedZone>
|
|
<Id>/hostedzone/{{ zone.id }}</Id>
|
|
<Name>{{ zone.name }}</Name>
|
|
<ResourceRecordSetCount>{{ zone.rrsets|count }}</ResourceRecordSetCount>
|
|
<Config>
|
|
{% if zone.comment %}
|
|
<Comment>{{ zone.comment }}</Comment>
|
|
{% endif %}
|
|
<PrivateZone>{{ zone.private_zone }}</PrivateZone>
|
|
</Config>
|
|
</HostedZone>
|
|
<DelegationSet>
|
|
<NameServers>
|
|
<NameServer>moto.test.com</NameServer>
|
|
</NameServers>
|
|
</DelegationSet>
|
|
</GetHostedZoneResponse>"""
|
|
|
|
CREATE_HOSTED_ZONE_RESPONSE = """<CreateHostedZoneResponse xmlns="https://route53.amazonaws.com/doc/2012-12-12/">
|
|
<HostedZone>
|
|
<Id>/hostedzone/{{ zone.id }}</Id>
|
|
<Name>{{ zone.name }}</Name>
|
|
<ResourceRecordSetCount>0</ResourceRecordSetCount>
|
|
<Config>
|
|
{% if zone.comment %}
|
|
<Comment>{{ zone.comment }}</Comment>
|
|
{% endif %}
|
|
<PrivateZone>{{ zone.private_zone }}</PrivateZone>
|
|
</Config>
|
|
</HostedZone>
|
|
<DelegationSet>
|
|
<NameServers>
|
|
<NameServer>moto.test.com</NameServer>
|
|
</NameServers>
|
|
</DelegationSet>
|
|
</CreateHostedZoneResponse>"""
|
|
|
|
LIST_HOSTED_ZONES_RESPONSE = """<ListHostedZonesResponse xmlns="https://route53.amazonaws.com/doc/2012-12-12/">
|
|
<HostedZones>
|
|
{% for zone in zones %}
|
|
<HostedZone>
|
|
<Id>/hostedzone/{{ zone.id }}</Id>
|
|
<Name>{{ zone.name }}</Name>
|
|
<Config>
|
|
{% if zone.comment %}
|
|
<Comment>{{ zone.comment }}</Comment>
|
|
{% endif %}
|
|
<PrivateZone>{{ zone.private_zone }}</PrivateZone>
|
|
</Config>
|
|
<ResourceRecordSetCount>{{ zone.rrsets|count }}</ResourceRecordSetCount>
|
|
</HostedZone>
|
|
{% endfor %}
|
|
</HostedZones>
|
|
<IsTruncated>false</IsTruncated>
|
|
</ListHostedZonesResponse>"""
|
|
|
|
LIST_HOSTED_ZONES_BY_NAME_RESPONSE = """<ListHostedZonesByNameResponse xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
|
|
{% if dnsname %}
|
|
<DNSName>{{ dnsname }}</DNSName>
|
|
{% endif %}
|
|
<HostedZones>
|
|
{% for zone in zones %}
|
|
<HostedZone>
|
|
<Id>/hostedzone/{{ zone.id }}</Id>
|
|
<Name>{{ zone.name }}</Name>
|
|
<Config>
|
|
{% if zone.comment %}
|
|
<Comment>{{ zone.comment }}</Comment>
|
|
{% endif %}
|
|
<PrivateZone>{{ zone.private_zone }}</PrivateZone>
|
|
</Config>
|
|
<ResourceRecordSetCount>{{ zone.rrsets|count }}</ResourceRecordSetCount>
|
|
</HostedZone>
|
|
{% endfor %}
|
|
</HostedZones>
|
|
<IsTruncated>false</IsTruncated>
|
|
</ListHostedZonesByNameResponse>"""
|
|
|
|
CREATE_HEALTH_CHECK_RESPONSE = """<?xml version="1.0" encoding="UTF-8"?>
|
|
<CreateHealthCheckResponse xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
|
|
{{ health_check.to_xml() }}
|
|
</CreateHealthCheckResponse>"""
|
|
|
|
LIST_HEALTH_CHECKS_RESPONSE = """<?xml version="1.0" encoding="UTF-8"?>
|
|
<ListHealthChecksResponse xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
|
|
<HealthChecks>
|
|
{% for health_check in health_checks %}
|
|
{{ health_check.to_xml() }}
|
|
{% endfor %}
|
|
</HealthChecks>
|
|
<IsTruncated>false</IsTruncated>
|
|
<MaxItems>{{ health_checks|length }}</MaxItems>
|
|
</ListHealthChecksResponse>"""
|
|
|
|
DELETE_HEALTH_CHECK_RESPONSE = """<?xml version="1.0" encoding="UTF-8"?>
|
|
<DeleteHealthCheckResponse xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
|
|
</DeleteHealthCheckResponse>"""
|
|
|
|
GET_CHANGE_RESPONSE = """<?xml version="1.0" encoding="UTF-8"?>
|
|
<GetChangeResponse xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
|
|
<ChangeInfo>
|
|
<Status>INSYNC</Status>
|
|
<SubmittedAt>2010-09-10T01:36:41.958Z</SubmittedAt>
|
|
<Id>{{ change_id }}</Id>
|
|
</ChangeInfo>
|
|
</GetChangeResponse>"""
|