Home | History | Annotate | Download | only in route53
      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 
     25 RECORD_TYPES = ['A', 'AAAA', 'TXT', 'CNAME', 'MX', 'PTR', 'SRV', 'SPF']
     26 
     27 from boto.resultset import ResultSet
     28 
     29 
     30 class 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 
    199 class 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