• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
25default_ttl = 60
26
27import copy
28from boto.exception import TooManyRecordsException
29from boto.route53.record import ResourceRecordSets
30from boto.route53.status import Status
31
32
33class 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