1# Copyright (c) 2010 Chris Moyer http://coredumped.org/
2# Copyright (c) 2012 Mitch Garnaat http://garnaat.org/
3# Copyright (c) 2012 Amazon.com, Inc. or its affiliates.
4# All rights reserved.
5#
6# Permission is hereby granted, free of charge, to any person obtaining a
7# copy of this software and associated documentation files (the
8# "Software"), to deal in the Software without restriction, including
9# without limitation the rights to use, copy, modify, merge, publish, dis-
10# tribute, sublicense, and/or sell copies of the Software, and to permit
11# persons to whom the Software is furnished to do so, subject to the fol-
12# lowing conditions:
13#
14# The above copyright notice and this permission notice shall be included
15# in all copies or substantial portions of the Software.
16#
17# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
18# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
19# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
20# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
21# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
23# IN THE SOFTWARE.
24
25RECORD_TYPES = ['A', 'AAAA', 'TXT', 'CNAME', 'MX', 'PTR', 'SRV', 'SPF']
26
27from boto.resultset import ResultSet
28
29
30class ResourceRecordSets(ResultSet):
31    """
32    A list of resource records.
33
34    :ivar hosted_zone_id: The ID of the hosted zone.
35    :ivar comment: A comment that will be stored with the change.
36    :ivar changes: A list of changes.
37    """
38
39    ChangeResourceRecordSetsBody = """<?xml version="1.0" encoding="UTF-8"?>
40    <ChangeResourceRecordSetsRequest xmlns="https://route53.amazonaws.com/doc/2013-04-01/">
41            <ChangeBatch>
42                <Comment>%(comment)s</Comment>
43                <Changes>%(changes)s</Changes>
44            </ChangeBatch>
45        </ChangeResourceRecordSetsRequest>"""
46
47    ChangeXML = """<Change>
48        <Action>%(action)s</Action>
49        %(record)s
50    </Change>"""
51
52    def __init__(self, connection=None, hosted_zone_id=None, comment=None):
53        self.connection = connection
54        self.hosted_zone_id = hosted_zone_id
55        self.comment = comment
56        self.changes = []
57        self.next_record_name = None
58        self.next_record_type = None
59        self.next_record_identifier = None
60        super(ResourceRecordSets, self).__init__([('ResourceRecordSet', Record)])
61
62    def __repr__(self):
63        if self.changes:
64            record_list = ','.join([c.__repr__() for c in self.changes])
65        else:
66            record_list = ','.join([record.__repr__() for record in self])
67        return '<ResourceRecordSets:%s [%s]' % (self.hosted_zone_id,
68                                                record_list)
69
70    def add_change(self, action, name, type, ttl=600,
71                   alias_hosted_zone_id=None, alias_dns_name=None, identifier=None,
72                   weight=None, region=None, alias_evaluate_target_health=None,
73                   health_check=None, failover=None):
74        """
75        Add a change request to the set.
76
77        :type action: str
78        :param action: The action to perform ('CREATE'|'DELETE'|'UPSERT')
79
80        :type name: str
81        :param name: The name of the domain you want to perform the action on.
82
83        :type type: str
84        :param type: The DNS record type.  Valid values are:
85
86            * A
87            * AAAA
88            * CNAME
89            * MX
90            * NS
91            * PTR
92            * SOA
93            * SPF
94            * SRV
95            * TXT
96
97        :type ttl: int
98        :param ttl: The resource record cache time to live (TTL), in seconds.
99
100        :type alias_hosted_zone_id: str
101        :param alias_dns_name: *Alias resource record sets only* The value
102            of the hosted zone ID, CanonicalHostedZoneNameId, for
103            the LoadBalancer.
104
105        :type alias_dns_name: str
106        :param alias_hosted_zone_id: *Alias resource record sets only*
107            Information about the domain to which you are redirecting traffic.
108
109        :type identifier: str
110        :param identifier: *Weighted and latency-based resource record sets
111            only* An identifier that differentiates among multiple resource
112            record sets that have the same combination of DNS name and type.
113
114        :type weight: int
115        :param weight: *Weighted resource record sets only* Among resource
116            record sets that have the same combination of DNS name and type,
117            a value that determines what portion of traffic for the current
118            resource record set is routed to the associated location
119
120        :type region: str
121        :param region: *Latency-based resource record sets only* Among resource
122            record sets that have the same combination of DNS name and type,
123            a value that determines which region this should be associated with
124            for the latency-based routing
125
126        :type alias_evaluate_target_health: bool
127        :param alias_evaluate_target_health: *Required for alias resource record
128            sets* Indicates whether this Resource Record Set should respect the
129            health status of any health checks associated with the ALIAS target
130            record which it is linked to.
131
132        :type health_check: str
133        :param health_check: Health check to associate with this record
134
135        :type failover: str
136        :param failover: *Failover resource record sets only* Whether this is the
137            primary or secondary resource record set.
138        """
139        change = Record(name, type, ttl,
140                        alias_hosted_zone_id=alias_hosted_zone_id,
141                        alias_dns_name=alias_dns_name, identifier=identifier,
142                        weight=weight, region=region,
143                        alias_evaluate_target_health=alias_evaluate_target_health,
144                        health_check=health_check, failover=failover)
145        self.changes.append([action, change])
146        return change
147
148    def add_change_record(self, action, change):
149        """Add an existing record to a change set with the specified action"""
150        self.changes.append([action, change])
151        return
152
153    def to_xml(self):
154        """Convert this ResourceRecordSet into XML
155        to be saved via the ChangeResourceRecordSetsRequest"""
156        changesXML = ""
157        for change in self.changes:
158            changeParams = {"action": change[0], "record": change[1].to_xml()}
159            changesXML += self.ChangeXML % changeParams
160        params = {"comment": self.comment, "changes": changesXML}
161        return self.ChangeResourceRecordSetsBody % params
162
163    def commit(self):
164        """Commit this change"""
165        if not self.connection:
166            import boto
167            self.connection = boto.connect_route53()
168        return self.connection.change_rrsets(self.hosted_zone_id, self.to_xml())
169
170    def endElement(self, name, value, connection):
171        """Overwritten to also add the NextRecordName,
172        NextRecordType and NextRecordIdentifier to the base object"""
173        if name == 'NextRecordName':
174            self.next_record_name = value
175        elif name == 'NextRecordType':
176            self.next_record_type = value
177        elif name == 'NextRecordIdentifier':
178            self.next_record_identifier = value
179        else:
180            return super(ResourceRecordSets, self).endElement(name, value, connection)
181
182    def __iter__(self):
183        """Override the next function to support paging"""
184        results = super(ResourceRecordSets, self).__iter__()
185        truncated = self.is_truncated
186        while results:
187            for obj in results:
188                yield obj
189            if self.is_truncated:
190                self.is_truncated = False
191                results = self.connection.get_all_rrsets(self.hosted_zone_id, name=self.next_record_name,
192                                                         type=self.next_record_type,
193                                                         identifier=self.next_record_identifier)
194            else:
195                results = None
196                self.is_truncated = truncated
197
198
199class Record(object):
200    """An individual ResourceRecordSet"""
201
202    HealthCheckBody = """<HealthCheckId>%s</HealthCheckId>"""
203
204    XMLBody = """<ResourceRecordSet>
205        <Name>%(name)s</Name>
206        <Type>%(type)s</Type>
207        %(weight)s
208        %(body)s
209        %(health_check)s
210    </ResourceRecordSet>"""
211
212    WRRBody = """
213        <SetIdentifier>%(identifier)s</SetIdentifier>
214        <Weight>%(weight)s</Weight>
215    """
216
217    RRRBody = """
218        <SetIdentifier>%(identifier)s</SetIdentifier>
219        <Region>%(region)s</Region>
220    """
221
222    FailoverBody = """
223        <SetIdentifier>%(identifier)s</SetIdentifier>
224        <Failover>%(failover)s</Failover>
225    """
226
227    ResourceRecordsBody = """
228        <TTL>%(ttl)s</TTL>
229        <ResourceRecords>
230            %(records)s
231        </ResourceRecords>"""
232
233    ResourceRecordBody = """<ResourceRecord>
234        <Value>%s</Value>
235    </ResourceRecord>"""
236
237    AliasBody = """<AliasTarget>
238        <HostedZoneId>%(hosted_zone_id)s</HostedZoneId>
239        <DNSName>%(dns_name)s</DNSName>
240        %(eval_target_health)s
241    </AliasTarget>"""
242
243    EvaluateTargetHealth = """<EvaluateTargetHealth>%s</EvaluateTargetHealth>"""
244
245    def __init__(self, name=None, type=None, ttl=600, resource_records=None,
246                 alias_hosted_zone_id=None, alias_dns_name=None, identifier=None,
247                 weight=None, region=None, alias_evaluate_target_health=None,
248                 health_check=None, failover=None):
249        self.name = name
250        self.type = type
251        self.ttl = ttl
252        if resource_records is None:
253            resource_records = []
254        self.resource_records = resource_records
255        self.alias_hosted_zone_id = alias_hosted_zone_id
256        self.alias_dns_name = alias_dns_name
257        self.identifier = identifier
258        self.weight = weight
259        self.region = region
260        self.alias_evaluate_target_health = alias_evaluate_target_health
261        self.health_check = health_check
262        self.failover = failover
263
264    def __repr__(self):
265        return '<Record:%s:%s:%s>' % (self.name, self.type, self.to_print())
266
267    def add_value(self, value):
268        """Add a resource record value"""
269        self.resource_records.append(value)
270
271    def set_alias(self, alias_hosted_zone_id, alias_dns_name,
272                  alias_evaluate_target_health=False):
273        """Make this an alias resource record set"""
274        self.alias_hosted_zone_id = alias_hosted_zone_id
275        self.alias_dns_name = alias_dns_name
276        self.alias_evaluate_target_health = alias_evaluate_target_health
277
278    def to_xml(self):
279        """Spit this resource record set out as XML"""
280        if self.alias_hosted_zone_id is not None and self.alias_dns_name is not None:
281            # Use alias
282            if self.alias_evaluate_target_health is not None:
283                eval_target_health = self.EvaluateTargetHealth % ('true' if self.alias_evaluate_target_health else 'false')
284            else:
285                eval_target_health = ""
286
287            body = self.AliasBody % {"hosted_zone_id": self.alias_hosted_zone_id,
288                                     "dns_name": self.alias_dns_name,
289                                     "eval_target_health": eval_target_health}
290        else:
291            # Use resource record(s)
292            records = ""
293
294            for r in self.resource_records:
295                records += self.ResourceRecordBody % r
296
297            body = self.ResourceRecordsBody % {
298                "ttl": self.ttl,
299                "records": records,
300            }
301
302        weight = ""
303
304        if self.identifier is not None and self.weight is not None:
305            weight = self.WRRBody % {"identifier": self.identifier,
306                                     "weight": self.weight}
307        elif self.identifier is not None and self.region is not None:
308            weight = self.RRRBody % {"identifier": self.identifier,
309                                     "region": self.region}
310        elif self.identifier is not None and self.failover is not None:
311            weight = self.FailoverBody % {"identifier": self.identifier,
312                                          "failover": self.failover}
313
314        health_check = ""
315        if self.health_check is not None:
316            health_check = self.HealthCheckBody % (self.health_check)
317
318        params = {
319            "name": self.name,
320            "type": self.type,
321            "weight": weight,
322            "body": body,
323            "health_check": health_check
324        }
325        return self.XMLBody % params
326
327    def to_print(self):
328        rr = ""
329        if self.alias_hosted_zone_id is not None and self.alias_dns_name is not None:
330            # Show alias
331            rr = 'ALIAS ' + self.alias_hosted_zone_id + ' ' + self.alias_dns_name
332            if self.alias_evaluate_target_health is not None:
333                rr += ' (EvalTarget %s)' % self.alias_evaluate_target_health
334        else:
335            # Show resource record(s)
336            rr = ",".join(self.resource_records)
337
338        if self.identifier is not None and self.weight is not None:
339            rr += ' (WRR id=%s, w=%s)' % (self.identifier, self.weight)
340        elif self.identifier is not None and self.region is not None:
341            rr += ' (LBR id=%s, region=%s)' % (self.identifier, self.region)
342        elif self.identifier is not None and self.failover is not None:
343            rr += ' (FAILOVER id=%s, failover=%s)' % (self.identifier, self.failover)
344
345        return rr
346
347    def endElement(self, name, value, connection):
348        if name == 'Name':
349            self.name = value
350        elif name == 'Type':
351            self.type = value
352        elif name == 'TTL':
353            self.ttl = value
354        elif name == 'Value':
355            self.resource_records.append(value)
356        elif name == 'HostedZoneId':
357            self.alias_hosted_zone_id = value
358        elif name == 'DNSName':
359            self.alias_dns_name = value
360        elif name == 'SetIdentifier':
361            self.identifier = value
362        elif name == 'EvaluateTargetHealth':
363            self.alias_evaluate_target_health = value.lower() == 'true'
364        elif name == 'Weight':
365            self.weight = value
366        elif name == 'Region':
367            self.region = value
368        elif name == 'Failover':
369            self.failover = value
370        elif name == 'HealthCheckId':
371            self.health_check = value
372
373    def startElement(self, name, attrs, connection):
374        return None
375