Home | History | Annotate | Download | only in route53
      1 # Copyright (c) 2011 Blue Pines Technologies LLC, Brad Carleton
      2 # www.bluepines.org
      3 # Copyright (c) 2012 42 Lines Inc., Jim Browne
      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 default_ttl = 60
     26 
     27 import copy
     28 from boto.exception import TooManyRecordsException
     29 from boto.route53.record import ResourceRecordSets
     30 from boto.route53.status import Status
     31 
     32 
     33 class Zone(object):
     34     """
     35     A Route53 Zone.
     36 
     37     :ivar route53connection: A :class:`boto.route53.connection.Route53Connection` connection
     38     :ivar id: The ID of the hosted zone
     39     """
     40     def __init__(self, route53connection, zone_dict):
     41         self.route53connection = route53connection
     42         for key in zone_dict:
     43             if key == 'Id':
     44                 self.id = zone_dict['Id'].replace('/hostedzone/', '')
     45             else:
     46                 self.__setattr__(key.lower(), zone_dict[key])
     47 
     48     def __repr__(self):
     49         return '<Zone:%s>' % self.name
     50 
     51     def _commit(self, changes):
     52         """
     53         Commit a set of changes and return the ChangeInfo portion of
     54         the response.
     55 
     56         :type changes: ResourceRecordSets
     57         :param changes: changes to be committed
     58         """
     59         response = changes.commit()
     60         return response['ChangeResourceRecordSetsResponse']['ChangeInfo']
     61 
     62     def _new_record(self, changes, resource_type, name, value, ttl, identifier,
     63                     comment=""):
     64         """
     65         Add a CREATE change record to an existing ResourceRecordSets
     66 
     67         :type changes: ResourceRecordSets
     68         :param changes: change set to append to
     69 
     70         :type name: str
     71         :param name: The name of the resource record you want to
     72             perform the action on.
     73 
     74         :type resource_type: str
     75         :param resource_type: The DNS record type
     76 
     77         :param value: Appropriate value for resource_type
     78 
     79         :type ttl: int
     80         :param ttl: The resource record cache time to live (TTL), in seconds.
     81 
     82         :type identifier: tuple
     83         :param identifier: A tuple for setting WRR or LBR attributes.  Valid
     84            forms are:
     85 
     86            * (str, int): WRR record [e.g. ('foo',10)]
     87            * (str, str): LBR record [e.g. ('foo','us-east-1')
     88 
     89         :type comment: str
     90         :param comment: A comment that will be stored with the change.
     91         """
     92         weight = None
     93         region = None
     94         if identifier is not None:
     95             try:
     96                 int(identifier[1])
     97                 weight = identifier[1]
     98                 identifier = identifier[0]
     99             except:
    100                 region = identifier[1]
    101                 identifier = identifier[0]
    102         change = changes.add_change("CREATE", name, resource_type, ttl,
    103                                     identifier=identifier, weight=weight,
    104                                     region=region)
    105         if type(value) in [list, tuple, set]:
    106             for record in value:
    107                 change.add_value(record)
    108         else:
    109             change.add_value(value)
    110 
    111     def add_record(self, resource_type, name, value, ttl=60, identifier=None,
    112                    comment=""):
    113         """
    114         Add a new record to this Zone.  See _new_record for parameter
    115         documentation.  Returns a Status object.
    116         """
    117         changes = ResourceRecordSets(self.route53connection, self.id, comment)
    118         self._new_record(changes, resource_type, name, value, ttl, identifier,
    119                          comment)
    120         return Status(self.route53connection, self._commit(changes))
    121 
    122     def update_record(self, old_record, new_value, new_ttl=None,
    123                       new_identifier=None, comment=""):
    124         """
    125         Update an existing record in this Zone.  Returns a Status object.
    126 
    127         :type old_record: ResourceRecord
    128         :param old_record: A ResourceRecord (e.g. returned by find_records)
    129 
    130         See _new_record for additional parameter documentation.
    131         """
    132         new_ttl = new_ttl or default_ttl
    133         record = copy.copy(old_record)
    134         changes = ResourceRecordSets(self.route53connection, self.id, comment)
    135         changes.add_change_record("DELETE", record)
    136         self._new_record(changes, record.type, record.name,
    137                          new_value, new_ttl, new_identifier, comment)
    138         return Status(self.route53connection, self._commit(changes))
    139 
    140     def delete_record(self, record, comment=""):
    141         """
    142         Delete one or more records from this Zone.  Returns a Status object.
    143 
    144         :param record: A ResourceRecord (e.g. returned by
    145            find_records) or list, tuple, or set of ResourceRecords.
    146 
    147         :type comment: str
    148         :param comment: A comment that will be stored with the change.
    149         """
    150         changes = ResourceRecordSets(self.route53connection, self.id, comment)
    151         if type(record) in [list, tuple, set]:
    152             for r in record:
    153                 changes.add_change_record("DELETE", r)
    154         else:
    155             changes.add_change_record("DELETE", record)
    156         return Status(self.route53connection, self._commit(changes))
    157 
    158     def add_cname(self, name, value, ttl=None, identifier=None, comment=""):
    159         """
    160         Add a new CNAME record to this Zone.  See _new_record for
    161         parameter documentation.  Returns a Status object.
    162         """
    163         ttl = ttl or default_ttl
    164         name = self.route53connection._make_qualified(name)
    165         value = self.route53connection._make_qualified(value)
    166         return self.add_record(resource_type='CNAME',
    167                                name=name,
    168                                value=value,
    169                                ttl=ttl,
    170                                identifier=identifier,
    171                                comment=comment)
    172 
    173     def add_a(self, name, value, ttl=None, identifier=None, comment=""):
    174         """
    175         Add a new A record to this Zone.  See _new_record for
    176         parameter documentation.  Returns a Status object.
    177         """
    178         ttl = ttl or default_ttl
    179         name = self.route53connection._make_qualified(name)
    180         return self.add_record(resource_type='A',
    181                                name=name,
    182                                value=value,
    183                                ttl=ttl,
    184                                identifier=identifier,
    185                                comment=comment)
    186 
    187     def add_mx(self, name, records, ttl=None, identifier=None, comment=""):
    188         """
    189         Add a new MX record to this Zone.  See _new_record for
    190         parameter documentation.  Returns a Status object.
    191         """
    192         ttl = ttl or default_ttl
    193         records = self.route53connection._make_qualified(records)
    194         return self.add_record(resource_type='MX',
    195                                name=name,
    196                                value=records,
    197                                ttl=ttl,
    198                                identifier=identifier,
    199                                comment=comment)
    200 
    201     def find_records(self, name, type, desired=1, all=False, identifier=None):
    202         """
    203         Search this Zone for records that match given parameters.
    204         Returns None if no results, a ResourceRecord if one result, or
    205         a ResourceRecordSets if more than one result.
    206 
    207         :type name: str
    208         :param name: The name of the records should match this parameter
    209 
    210         :type type: str
    211         :param type: The type of the records should match this parameter
    212 
    213         :type desired: int
    214         :param desired: The number of desired results.  If the number of
    215            matching records in the Zone exceeds the value of this parameter,
    216            throw TooManyRecordsException
    217 
    218         :type all: Boolean
    219         :param all: If true return all records that match name, type, and
    220           identifier parameters
    221 
    222         :type identifier: Tuple
    223         :param identifier: A tuple specifying WRR or LBR attributes.  Valid
    224            forms are:
    225 
    226            * (str, int): WRR record [e.g. ('foo',10)]
    227            * (str, str): LBR record [e.g. ('foo','us-east-1')
    228 
    229         """
    230         name = self.route53connection._make_qualified(name)
    231         returned = self.route53connection.get_all_rrsets(self.id, name=name,
    232                                                          type=type)
    233 
    234         # name/type for get_all_rrsets sets the starting record; they
    235         # are not a filter
    236         results = []
    237         for r in returned:
    238             if r.name == name and r.type == type:
    239                 results.append(r)
    240             # Is at the end of the list of matched records. No need to continue
    241             # since the records are sorted by name and type.
    242             else:
    243                 break
    244 
    245         weight = None
    246         region = None
    247         if identifier is not None:
    248             try:
    249                 int(identifier[1])
    250                 weight = identifier[1]
    251             except:
    252                 region = identifier[1]
    253 
    254         if weight is not None:
    255             results = [r for r in results if (r.weight == weight and
    256                                               r.identifier == identifier[0])]
    257         if region is not None:
    258             results = [r for r in results if (r.region == region and
    259                                               r.identifier == identifier[0])]
    260 
    261         if ((not all) and (len(results) > desired)):
    262             message = "Search: name %s type %s" % (name, type)
    263             message += "\nFound: "
    264             message += ", ".join(["%s %s %s" % (r.name, r.type, r.to_print())
    265                                   for r in results])
    266             raise TooManyRecordsException(message)
    267         elif len(results) > 1:
    268             return results
    269         elif len(results) == 1:
    270             return results[0]
    271         else:
    272             return None
    273 
    274     def get_cname(self, name, all=False):
    275         """
    276         Search this Zone for CNAME records that match name.
    277 
    278         Returns a ResourceRecord.
    279 
    280         If there is more than one match return all as a
    281         ResourceRecordSets if all is True, otherwise throws
    282         TooManyRecordsException.
    283         """
    284         return self.find_records(name, 'CNAME', all=all)
    285 
    286     def get_a(self, name, all=False):
    287         """
    288         Search this Zone for A records that match name.
    289 
    290         Returns a ResourceRecord.
    291 
    292         If there is more than one match return all as a
    293         ResourceRecordSets if all is True, otherwise throws
    294         TooManyRecordsException.
    295         """
    296         return self.find_records(name, 'A', all=all)
    297 
    298     def get_mx(self, name, all=False):
    299         """
    300         Search this Zone for MX records that match name.
    301 
    302         Returns a ResourceRecord.
    303 
    304         If there is more than one match return all as a
    305         ResourceRecordSets if all is True, otherwise throws
    306         TooManyRecordsException.
    307         """
    308         return self.find_records(name, 'MX', all=all)
    309 
    310     def update_cname(self, name, value, ttl=None, identifier=None, comment=""):
    311         """
    312         Update the given CNAME record in this Zone to a new value, ttl,
    313         and identifier.  Returns a Status object.
    314 
    315         Will throw TooManyRecordsException is name, value does not match
    316         a single record.
    317         """
    318         name = self.route53connection._make_qualified(name)
    319         value = self.route53connection._make_qualified(value)
    320         old_record = self.get_cname(name)
    321         ttl = ttl or old_record.ttl
    322         return self.update_record(old_record,
    323                                   new_value=value,
    324                                   new_ttl=ttl,
    325                                   new_identifier=identifier,
    326                                   comment=comment)
    327 
    328     def update_a(self, name, value, ttl=None, identifier=None, comment=""):
    329         """
    330         Update the given A record in this Zone to a new value, ttl,
    331         and identifier.  Returns a Status object.
    332 
    333         Will throw TooManyRecordsException is name, value does not match
    334         a single record.
    335         """
    336         name = self.route53connection._make_qualified(name)
    337         old_record = self.get_a(name)
    338         ttl = ttl or old_record.ttl
    339         return self.update_record(old_record,
    340                                   new_value=value,
    341                                   new_ttl=ttl,
    342                                   new_identifier=identifier,
    343                                   comment=comment)
    344 
    345     def update_mx(self, name, value, ttl=None, identifier=None, comment=""):
    346         """
    347         Update the given MX record in this Zone to a new value, ttl,
    348         and identifier.  Returns a Status object.
    349 
    350         Will throw TooManyRecordsException is name, value does not match
    351         a single record.
    352         """
    353         name = self.route53connection._make_qualified(name)
    354         value = self.route53connection._make_qualified(value)
    355         old_record = self.get_mx(name)
    356         ttl = ttl or old_record.ttl
    357         return self.update_record(old_record,
    358                                   new_value=value,
    359                                   new_ttl=ttl,
    360                                   new_identifier=identifier,
    361                                   comment=comment)
    362 
    363     def delete_cname(self, name, identifier=None, all=False):
    364         """
    365         Delete a CNAME record matching name and identifier from
    366         this Zone.  Returns a Status object.
    367 
    368         If there is more than one match delete all matching records if
    369         all is True, otherwise throws TooManyRecordsException.
    370         """
    371         name = self.route53connection._make_qualified(name)
    372         record = self.find_records(name, 'CNAME', identifier=identifier,
    373                                    all=all)
    374         return self.delete_record(record)
    375 
    376     def delete_a(self, name, identifier=None, all=False):
    377         """
    378         Delete an A record matching name and identifier from this
    379         Zone.  Returns a Status object.
    380 
    381         If there is more than one match delete all matching records if
    382         all is True, otherwise throws TooManyRecordsException.
    383         """
    384         name = self.route53connection._make_qualified(name)
    385         record = self.find_records(name, 'A', identifier=identifier,
    386                                    all=all)
    387         return self.delete_record(record)
    388 
    389     def delete_mx(self, name, identifier=None, all=False):
    390         """
    391         Delete an MX record matching name and identifier from this
    392         Zone.  Returns a Status object.
    393 
    394         If there is more than one match delete all matching records if
    395         all is True, otherwise throws TooManyRecordsException.
    396         """
    397         name = self.route53connection._make_qualified(name)
    398         record = self.find_records(name, 'MX', identifier=identifier,
    399                                    all=all)
    400         return self.delete_record(record)
    401 
    402     def get_records(self):
    403         """
    404         Return a ResourceRecordsSets for all of the records in this zone.
    405         """
    406         return self.route53connection.get_all_rrsets(self.id)
    407 
    408     def delete(self):
    409         """
    410         Request that this zone be deleted by Amazon.
    411         """
    412         self.route53connection.delete_hosted_zone(self.id)
    413 
    414     def get_nameservers(self):
    415         """ Get the list of nameservers for this zone."""
    416         ns = self.find_records(self.name, 'NS')
    417         if ns is not None:
    418             ns = ns.resource_records
    419         return ns
    420