def create_hosted_zone(self, caller_reference, name): """ @see: L{txaws.route53.client._Route53Client.create_hosted_zone} """ zone = HostedZone( name=name, reference=caller_reference, identifier=self._state.next_id(), # Hosted zones start with SOA and NS rrsets. rrset_count=2, ) self._state.zones = self._state.zones.append(zone) self.change_resource_record_sets( zone.identifier, [ create_rrset( RRSet( label=Name(name), type="SOA", ttl=900, records=self._state.soa_records, ), ), create_rrset( RRSet( label=Name(name), type="NS", ttl=172800, records=self._state.ns_records, ), ), ], ) return succeed(zone)
def test_list_resource_record_sets_maxitems(self): """ If C{maxitems} is used to limit the number of records returned by C{list_resource_record_sets}, the records returned are those that sort first according to the rules given by U{http://docs.aws.amazon.com/Route53/latest/APIReference/API_ListResourceRecordSets.html#API_ListResourceRecordSets_RequestSyntax}. """ zone_name = u"{}.example.invalid.".format(uuid4()) client = get_client(self) # extra sorts _after_ expected according to the AWS Route53 # ordering rules but it sorts _before_ according to more naive # (incorrect) string ordering rules. extra = RRSet( Name(u"a.z.{}".format(zone_name)), u"A", 60, {A(IPv4Address(u"10.0.0.1"))}, ) expected = RRSet( Name(u"b.y.{}".format(zone_name)), u"A", 60, {A(IPv4Address(u"10.0.0.2"))}, ) d = client.create_hosted_zone(u"{}".format(time()), zone_name) def created_zone(zone): self.addCleanup(lambda: self._cleanup(client, zone.identifier)) d = client.change_resource_record_sets(zone.identifier, [ create_rrset(extra), create_rrset(expected), ]) d.addCallback(lambda ignored: zone) return d d.addCallback(created_zone) def created_rrsets(zone): return client.list_resource_record_sets( zone.identifier, name=Name(u"a.{}".format(zone_name)), type=u"A", maxitems=1, ) d.addCallback(created_rrsets) def listed_rrsets(rrsets): self.assertEqual( {RRSetKey(expected.label, expected.type): expected}, rrsets, ) d.addCallback(listed_rrsets) return d
def _rrset_for_subscription(subscription_id, zone_name): return RRSet( label=_introducer_name_for_subscription(subscription_id, zone_name), type=u"CNAME", ttl=60, records={_cname_for_subscription(zone_name)}, )
def test_delete_missing_rrset(self): """ It is an error to attempt to delete an rrset which does not exist. """ zone_name = u"{}.test_delete_missing_rrset.invalid.".format( uuid4()) rrset = RRSet( label=Name(u"foo.{}".format(zone_name)), type=u"CNAME", ttl=60, records={CNAME(canonical_name=Name(u"bar.example.invalid."))}, ) client = get_client(self) d = client.create_hosted_zone(u"{}".format(time()), zone_name) def created_zone(zone): self.addCleanup(lambda: self._cleanup(client, zone.identifier)) d = client.change_resource_record_sets(zone.identifier, [delete_rrset(rrset)]) self.assertFailure(d, Route53Error) return d d.addCallback(created_zone) def got_error(error): self.assertEqual(BAD_REQUEST, int(error.status)) d.addCallback(got_error) return d
def check_route53(self, database, config, subscriptions, k8s_state, aws): expected_rrsets = pmap() service = k8s_state.services.item_by_name(S4_CUSTOMER_GRID_NAME) if service.status is not None and service.status.loadBalancer.ingress: # TODO: It would be slightly nicer to make this a Route53 Alias # instead of a CNAME. txAWS needs support for creating Route53 Alias # rrsets first, though. introducer = RRSetKey( label=Name( u"introducer.{domain}".format(domain=config.domain)), type=u"CNAME", ) service_ingress = RRSet( label=introducer.label, type=introducer.type, ttl=60, records={ CNAME(canonical_name=Name( service.status.loadBalancer.ingress[0].hostname, ), ), }, ) expected_rrsets = expected_rrsets.set(introducer, service_ingress) for sid in subscriptions: label = _introducer_name_for_subscription(sid, config.domain) key = RRSetKey(label=label, type=u"CNAME") cname = CNAME( Name(u"introducer.{domain}".format(domain=config.domain))) rrset = RRSet(label=label, type=u"CNAME", ttl=60, records={cname}) expected_rrsets = expected_rrsets.set(key, rrset) route53 = aws.get_route53_client() d = route53.list_resource_record_sets(self.zone.identifier) result = self.case.successResultOf(d) actual_rrsets = pmap({ key: rrset for (key, rrset) in result.iteritems() # Don't care about these infrastructure rrsets. if key.type not in (u"SOA", u"NS") }) assert_that( actual_rrsets, GoodEquals(expected_rrsets), )
def test_soa_ns_cname(self): zone_id = b"ABCDEF1234" client = self._client_for_rrsets( zone_id, sample_list_resource_record_sets_result.xml, ) rrsets = self.successResultOf( client.list_resource_record_sets(zone_id=zone_id, )) expected = { RRSetKey( label=sample_list_resource_record_sets_result.label, type=u"SOA", ): RRSet( label=sample_list_resource_record_sets_result.label, type=u"SOA", ttl=sample_list_resource_record_sets_result.soa_ttl, records={sample_list_resource_record_sets_result.soa}, ), RRSetKey( label=sample_list_resource_record_sets_result.label, type=u"NS", ): RRSet( label=sample_list_resource_record_sets_result.label, type=u"NS", ttl=sample_list_resource_record_sets_result.ns_ttl, records={ sample_list_resource_record_sets_result.ns1, sample_list_resource_record_sets_result.ns2, }, ), RRSetKey( label=sample_list_resource_record_sets_result.label, type=u"CNAME", ): RRSet( label=sample_list_resource_record_sets_result.label, type=u"CNAME", ttl=sample_list_resource_record_sets_result.cname_ttl, records={sample_list_resource_record_sets_result.cname}, ), } self.assertEquals(rrsets, expected)
class sample_change_resource_record_sets_result(object): rrset = RRSet( label=Name(u"example.invalid."), type=u"NS", ttl=86400, records={ NS(Name(u"ns1.example.invalid.")), NS(Name(u"ns2.example.invalid.")), }, ) xml = b"""\
class sample_list_resource_records_with_alias_result(object): normal_target = Name(u"bar.example.invalid.") normal = RRSet( label=Name(u"foo.example.invalid."), type=u"CNAME", ttl=60, records={CNAME(canonical_name=normal_target)}, ) normal_xml = u"""\ <ResourceRecordSet><Name>{label}</Name><Type>{type}</Type><TTL>{ttl}</TTL><ResourceRecords><ResourceRecord><Value>{value}</Value></ResourceRecord></ResourceRecords></ResourceRecordSet> """.format( label=normal.label, type=normal.type, ttl=normal.ttl, value=normal_target, ) alias = AliasRRSet( label=Name(u"bar.example.invalid."), type=u"A", dns_name=Name( u"dualstack.a952f315901e6b3c812e57076f5b4138-0795221525.us-east-1.elb.amazonaws.com.", ), evaluate_target_health=False, hosted_zone_id=u"ZSXD5Q7O3X7TRK", ) alias_xml = u"""\ <ResourceRecordSet><Name>{label}</Name><Type>{type}</Type><AliasTarget><HostedZoneId>{hosted_zone_id}</HostedZoneId><DNSName>{dns_name}</DNSName><EvaluateTargetHealth>{evaluate_target_health}</EvaluateTargetHealth></AliasTarget></ResourceRecordSet> """.format( label=alias.label, type=alias.type, hosted_zone_id=alias.hosted_zone_id, dns_name=alias.dns_name, evaluate_target_health=[u"false", u"true"][alias.evaluate_target_health], ) xml = u"""\ <?xml version="1.0"?>\n <ListResourceRecordSetsResponse xmlns="https://route53.amazonaws.com/doc/2013-04-01/"><ResourceRecordSets>{normal}{alias}</ResourceRecordSets><IsTruncated>false</IsTruncated><MaxItems>100</MaxItems></ListResourceRecordSetsResponse> """.format(normal=normal_xml, alias=alias_xml).encode("utf-8")
class sample_create_resource_record_sets_error_result: label = Name("duplicate.example.invalid.") type = "CNAME" cname = CNAME(canonical_name=Name("somewhere.example.invalid."), ) rrset = RRSet( label=label, type="CNAME", ttl=600, records={cname}, ) xml = """\ <?xml version="1.0"?> <ErrorResponse xmlns="https://route53.amazonaws.com/doc/2013-04-01/"><Error><Type>Sender</Type><Code>InvalidChangeBatch</Code><Message>[Tried to create resource record set [name='{label}', type='{type}'] but it already exists]</Message></Error><RequestId>9197fef4-03cc-11e9-b35f-7947070744f2</RequestId></ErrorResponse> """.format( label=label, type=type, )
def test_change_resource_record_sets_nonexistent_zone(self): """ You cannot interact with resource record sets for a non-existent zone. """ rrset = RRSet( label=Name(u"foo.example.invalid."), type=u"CNAME", ttl=60, records={CNAME(canonical_name=Name(u"bar.example.invalid."))}, ) client = get_client(self) d = client.change_resource_record_sets(u"abcdefg12345678", [create_rrset(rrset)]) self.assertFailure(d, Route53Error) def got_error(error): self.assertEqual(NOT_FOUND, int(error.status)) d.addCallback(got_error) return d
def test_unknown_record_type(self): zone_id = b"ABCDEF1234" template = u"""\ <?xml version="1.0"?> <ListResourceRecordSetsResponse xmlns="https://route53.amazonaws.com/doc/2013-04-01/"> <ResourceRecordSets> <ResourceRecordSet> <Name>{label}</Name> <Type>{type}</Type> <TTL>{ttl}</TTL> <ResourceRecords> <ResourceRecord><Value>{record}</Value></ResourceRecord> </ResourceRecords> </ResourceRecordSet> </ResourceRecordSets> <IsTruncated>false</IsTruncated> <MaxItems>100</MaxItems> </ListResourceRecordSetsResponse> """ label = Name(u"foo") client = self._client_for_rrsets( zone_id, template.format( label=label, type=u"X-TXAWS-FICTIONAL", ttl=60, record=u"good luck interpreting this", ).encode("utf-8")) expected = { RRSetKey(label=label, type=u"X-TXAWS-FICTIONAL"): RRSet( label=label, type=u"X-TXAWS-FICTIONAL", ttl=60, records={UnknownRecordType(u"good luck interpreting this")}, ), } rrsets = self.successResultOf( client.list_resource_record_sets(zone_id=zone_id), ) self.assertEquals(expected, rrsets)
def _simple_record_test(self, record_type, record): zone_id = b"ABCDEF1234" template = u"""\ <?xml version="1.0"?> <ListResourceRecordSetsResponse xmlns="https://route53.amazonaws.com/doc/2013-04-01/"> <ResourceRecordSets> <ResourceRecordSet> <Name>{label}</Name> <Type>{type}</Type> <TTL>{ttl}</TTL> <ResourceRecords> <ResourceRecord><Value>{record}</Value></ResourceRecord> </ResourceRecords> </ResourceRecordSet> </ResourceRecordSets> <IsTruncated>false</IsTruncated> <MaxItems>100</MaxItems> </ListResourceRecordSetsResponse> """ label = Name(u"foo") client = self._client_for_rrsets( zone_id, template.format( label=label, type=record_type, ttl=60, record=record.to_text(), ).encode("utf-8")) expected = { RRSetKey(label=label, type=record_type): RRSet( label=label, type=record_type, ttl=60, records={record}, ), } rrsets = self.successResultOf( client.list_resource_record_sets(zone_id=zone_id), ) self.assertEquals(expected, rrsets)
def _converge_route53_infrastructure(actual, config, subscriptions, k8s, aws): """ Converge on the desired Route53 state relating to general S4 infrastructure. Specifically, make sure there is an rrset for the ``introducer`` subdomain which points at the customer grid service's load balancer endpoint. """ if actual.service is None or actual.service.status is None: # Cannot do anything without a v1.Service or one without a populated # v1.ServiceStatus field. return [] if not actual.service.status.loadBalancer.ingress: # Also cannot do anything if we don't yet know what our ingress # address is. return [] loadbalancer_hostname = actual.service.status.loadBalancer.ingress[0].hostname introducer_key = RRSetKey(label=_introducer_domain(config.domain), type=u"CNAME") desired_rrset = RRSet( label=introducer_key.label, type=introducer_key.type, ttl=60, records={ CNAME(canonical_name=Name(loadbalancer_hostname)), }, ) actual_rrset = actual.zone.rrsets.get(introducer_key, None) if actual_rrset == desired_rrset: # Nothing to do. return [] # Create it or change it to what we want. route53 = aws.get_route53_client() return [ lambda: change_route53_rrsets(route53, actual.zone.zone, desired_rrset), ]
def _safe_get_rrset_RESOURCE(self, label, type, rrset): # http://docs.aws.amazon.com/Route53/latest/APIReference/API_ResourceRecord.html resourcerecords = rrset.find("./ResourceRecords") if resourcerecords is None: return None records = resourcerecords.iterfind("./ResourceRecord") # The docs say TTL is optional but I think that means rrsets that # contain something other than ResourceRecord may not have it. # Hopefully it's always present for ResourceRecord-tyle # ResourceRecordSets? ttl = int(rrset.find("TTL").text) return RRSet( label=label, type=type, ttl=ttl, records={ RECORD_TYPES[type].basic_from_element(element) for element in records # This is what's changed from upstream. We'll just drop anything # we don't recognize. That's sufficient for our purposes for now. if type in RECORD_TYPES }, )
def test_resource_record_sets(self): zone_name = u"{}.example.invalid.".format(uuid4()) cname = CNAME(canonical_name=Name(u"example.invalid.")) client = get_client(self) zone = yield client.create_hosted_zone(u"{}".format(time()), zone_name) # At least try to clean up, to be as nice as possible. # This might fail and someone else might have to do the # cleanup - but it might not! self.addCleanup(lambda: self._cleanup(client, zone.identifier)) cname_label = Name(u"foo.\N{SNOWMAN}.{}".format(zone_name)) create = create_rrset( RRSet( label=cname_label, type=u"CNAME", ttl=60, records={cname}, )) yield client.change_resource_record_sets(zone.identifier, [create]) initial = yield client.list_resource_record_sets(zone.identifier) key = RRSetKey(cname_label, u"CNAME") self.assertIn(key, initial) cname_rrset = initial[key] self.assertEqual( RRSet(label=cname_label, type=u"CNAME", ttl=60, records={cname}), cname_rrset, ) # Zones start with an SOA and some NS records. key = RRSetKey(Name(zone_name), u"SOA") self.assertIn(key, initial) soa = initial[key] self.assertEqual( len(soa.records), 1, "Expected one SOA record, got {}".format(soa.records)) key = RRSetKey(Name(zone_name), u"NS") self.assertIn(key, initial) ns = initial[key] self.assertNotEqual(set(), ns.records, "Expected some NS records, got none") # Unrecognized change type # XXX This depends on _ChangeRRSet using attrs. bogus = attr.assoc(create, action=u"BOGUS") d = client.change_resource_record_sets(zone.identifier, [bogus]) error = yield self.assertFailure(d, Route53Error) self.assertEqual(BAD_REQUEST, int(error.status)) created_a = A(IPv4Address(u"10.0.0.1")) upsert_label = Name(u"upsert.{}".format(zone_name)) upsert_create = upsert_rrset( RRSet( upsert_label, u"A", 60, {created_a}, )) updated_a = A(IPv4Address(u"10.0.0.2")) upsert_update = upsert_rrset( RRSet( upsert_create.rrset.label, upsert_create.rrset.type, upsert_create.rrset.ttl, {updated_a}, )) yield client.change_resource_record_sets(zone.identifier, [upsert_create]) rrsets = yield client.list_resource_record_sets(zone.identifier) self.assertEqual(rrsets[RRSetKey(upsert_label, u"A")].records, {created_a}) yield client.change_resource_record_sets(zone.identifier, [upsert_update]) rrsets = yield client.list_resource_record_sets(zone.identifier) self.assertEqual(rrsets[RRSetKey(upsert_label, u"A")].records, {updated_a}) # Use the name and maxitems parameters to select exactly one resource record. rrsets = yield client.list_resource_record_sets( zone.identifier, maxitems=1, name=upsert_label, type=u"A", ) self.assertEqual(1, len(rrsets), "Expected 1 rrset") self.assertEqual({updated_a}, rrsets[RRSetKey(upsert_label, u"A")].records) # It's invalid to specify type without name. d = client.list_resource_record_sets(zone.identifier, type=u"A") error = yield self.assertFailure(d, Route53Error) self.assertEqual(BAD_REQUEST, int(error.status)) # It's invalid to delete the SOA record. d = client.change_resource_record_sets( zone.identifier, [delete_rrset(soa)], ) error = yield self.assertFailure(d, Route53Error) self.assertEqual(BAD_REQUEST, int(error.status)) # Likewise, the NS records. d = client.change_resource_record_sets( zone.identifier, [delete_rrset(ns)], ) error = yield self.assertFailure(d, Route53Error) self.assertEqual(BAD_REQUEST, int(error.status)) # Test deletion at the end so the zone is clean for the # naive cleanup logic. yield client.change_resource_record_sets( zone.identifier, [ delete_rrset(cname_rrset), delete_rrset(upsert_update.rrset), ], ) rrsets = yield client.list_resource_record_sets(zone.identifier) self.assertNotIn(cname_label, rrsets) self.assertNotIn(upsert_label, rrsets)