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 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 created_rrsets(zone): return client.list_resource_record_sets( zone.identifier, name=Name(u"a.{}".format(zone_name)), type=u"A", maxitems=1, )
def _get_converge_inputs(config, subscriptions, k8s, aws): a = start_action(action_type=u"load-converge-inputs") with a.context(): d = DeferredContext( gatherResults([ get_active_subscriptions(subscriptions), get_customer_grid_configmaps(k8s, config.kubernetes_namespace), get_customer_grid_deployments(k8s, config.kubernetes_namespace), get_customer_grid_replicasets(k8s, config.kubernetes_namespace), get_customer_grid_pods(k8s, config.kubernetes_namespace), get_customer_grid_service(k8s, config.kubernetes_namespace), get_hosted_zone_by_name(aws.get_route53_client(), Name(config.domain)), ]), ) d.addCallback( lambda state: _State(**dict( zip([ u"subscriptions", u"configmaps", u"deployments", u"replicasets", u"pods", u"service", u"zone", ], state, ), )), ) return d.addActionFinish()
def _cname_for_subscription(domain): return CNAME( Name( u"introducer.{domain}".format( domain=domain, ) ) )
def itersubscription_ids(self): for key in self.rrsets: if key.type == u"CNAME": subscription_part, rest = key.label.text.split(u".", 1) # XXX Ugh strings if Name(rest) == _introducer_domain(self.domain): subscription_id = autopad_b32decode(subscription_part) yield subscription_id
def test_soa(self): """ L{SOA} can round-trip an I{SOA} record through XML. """ self._test_roundtrip( SOA, SOA( mname=Name("ns-857.example.invalid."), rname=Name("awsdns-hostmaster.example.invalid."), serial=1, refresh=7200, retry=900, expire=1209600, minimum=86400, ), XML(self._soa_xml), )
def test_cname(self): """ L{CNAME} can round-trip a I{CNAME} record through XML. """ self._test_roundtrip( CNAME, CNAME(canonical_name=Name("sub.example.invalid.")), XML(self._cname_xml), )
def test_ns(self): """ L{NS} can round-trip an I{NS} record through XML. """ self._test_roundtrip( NS, NS(nameserver=Name("ns1.example.invalid.")), XML(self._ns_xml), )
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 filter_results(zones): Message.log(zone_names=list(zone.name for zone in zones)) for zone in zones: # XXX Bleuch zone.name should be a Name! if Name(zone.name) == name: d = route53.list_resource_record_sets(zone_id=zone.identifier) d.addCallback( lambda rrsets, zone=zone: _ZoneState( zone=zone, rrsets=rrsets, ), ) return d raise KeyError(name)
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 _introducer_name_for_subscription(subscription_id, domain): return Name(configmap_public_host(subscription_id, domain))
def _introducer_domain(domain): """ Construct a ``Name`` for the intermediate domain name that glues per-user domain names to the load balancer hostname/address for the grid service. """ return Name(u"introducer.{}".format(domain))
class Route53ClientState: """ L{Route53ClientState} holds all of the Route53 state associated with a single account. This allows multiple clients with the same credentials to share state while hiding one account's state from other accounts (just as AWS does). @ivar soa_records: The SOA records (1) which will be put in all newly created hosted zones. @ivar ns_records: The NS records which will be put in all newly created hosted zones. @ivar zones: A sequence of HostedZone instances representing the zones known to exist. @type zones: L{pyrsistent.PVector} @ivar rrsets: A mapping from zone identifiers to further mappings. The further mappings map an L{RRSetKey} instance to an L{RRSet} instance and represent the rrsets belonging to the corresponding zone. @type rrsets: L{pyrsistent.PMap} """ soa_records = { SOA( mname=Name(text='ns-698.awsdns-23.net.example.invalid.'), rname=Name(text='awsdns-hostmaster.amazon.com.example.invalid.'), serial=1, refresh=7200, retry=900, expire=1209600, minimum=86400, ), } ns_records = { NS(nameserver=Name(text='ns-698.awsdns-23.net.example.invalid.')), NS(nameserver=Name(text='ns-1188.awsdns-20.org.examplie.invalid.')), } _id = attr.ib(default=attr.Factory(count), init=False) zones = attr.ib(default=pvector()) rrsets = attr.ib(default=pmap()) def next_id(self): """ Assign and return a new, unique hosted zone identifier. @rtype: L{str} """ return "/hostedzone/{:014d}".format(next(self._id)) def get_rrsets(self, zone_id): """ Retrieve all the rrsets that belong to the given zone. @param zone_id: The zone to inspect. @type zone_id: L{str} @return: L{None} if the zone is not found. Otherwise, a L{PMap} mapping L{RRSetKey} to L{RRSet}. """ if any(zone.identifier == zone_id for zone in self.zones): return self.rrsets.get(zone_id, pmap()) # You cannot interact with rrsets unless a zone exists. return None def set_rrsets(self, zone_id, rrsets): """ Specify all the rrsets that belong to the given zone. @param zone_id: The zone to modify. @type zone_id: L{str} @param rrsets: A L{PMap} mapping L{RRSetKey} to L{RRSet}. """ self.rrsets = self.rrsets.set(zone_id, rrsets)
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)