• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright (c) 2006-2010 Mitch Garnaat http://garnaat.org/
2# Copyright (c) 2010, Eucalyptus Systems, Inc.
3# Copyright (c) 2011 Blue Pines Technologies LLC, Brad Carleton
4# www.bluepines.org
5# Copyright (c) 2012 42 Lines Inc., Jim Browne
6#
7# Permission is hereby granted, free of charge, to any person obtaining a
8# copy of this software and associated documentation files (the
9# "Software"), to deal in the Software without restriction, including
10# without limitation the rights to use, copy, modify, merge, publish, dis-
11# tribute, sublicense, and/or sell copies of the Software, and to permit
12# persons to whom the Software is furnished to do so, subject to the fol-
13# lowing conditions:
14#
15# The above copyright notice and this permission notice shall be included
16# in all copies or substantial portions of the Software.
17#
18# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
19# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
20# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
21# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
22# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
24# IN THE SOFTWARE.
25#
26
27from boto.route53 import exception
28import random
29import uuid
30import xml.sax
31
32import boto
33from boto.connection import AWSAuthConnection
34from boto import handler
35import boto.jsonresponse
36from boto.route53.record import ResourceRecordSets
37from boto.route53.zone import Zone
38from boto.compat import six, urllib
39
40
41HZXML = """<?xml version="1.0" encoding="UTF-8"?>
42<CreateHostedZoneRequest xmlns="%(xmlns)s">
43  <Name>%(name)s</Name>
44  <CallerReference>%(caller_ref)s</CallerReference>
45  <HostedZoneConfig>
46    <Comment>%(comment)s</Comment>
47  </HostedZoneConfig>
48</CreateHostedZoneRequest>"""
49
50HZPXML = """<?xml version="1.0" encoding="UTF-8"?>
51<CreateHostedZoneRequest xmlns="%(xmlns)s">
52  <Name>%(name)s</Name>
53  <VPC>
54    <VPCId>%(vpc_id)s</VPCId>
55    <VPCRegion>%(vpc_region)s</VPCRegion>
56  </VPC>
57  <CallerReference>%(caller_ref)s</CallerReference>
58  <HostedZoneConfig>
59    <Comment>%(comment)s</Comment>
60  </HostedZoneConfig>
61</CreateHostedZoneRequest>"""
62
63# boto.set_stream_logger('dns')
64
65
66class Route53Connection(AWSAuthConnection):
67    DefaultHost = 'route53.amazonaws.com'
68    """The default Route53 API endpoint to connect to."""
69
70    Version = '2013-04-01'
71    """Route53 API version."""
72
73    XMLNameSpace = 'https://route53.amazonaws.com/doc/2013-04-01/'
74    """XML schema for this Route53 API version."""
75
76    def __init__(self, aws_access_key_id=None, aws_secret_access_key=None,
77                 port=None, proxy=None, proxy_port=None,
78                 host=DefaultHost, debug=0, security_token=None,
79                 validate_certs=True, https_connection_factory=None,
80                 profile_name=None):
81        super(Route53Connection, self).__init__(
82            host,
83            aws_access_key_id, aws_secret_access_key,
84            True, port, proxy, proxy_port, debug=debug,
85            security_token=security_token,
86            validate_certs=validate_certs,
87            https_connection_factory=https_connection_factory,
88            profile_name=profile_name)
89
90    def _required_auth_capability(self):
91        return ['route53']
92
93    def make_request(self, action, path, headers=None, data='', params=None):
94        if params:
95            pairs = []
96            for key, val in six.iteritems(params):
97                if val is None:
98                    continue
99                pairs.append(key + '=' + urllib.parse.quote(str(val)))
100            path += '?' + '&'.join(pairs)
101        return super(Route53Connection, self).make_request(
102            action, path, headers, data,
103            retry_handler=self._retry_handler)
104
105    # Hosted Zones
106
107    def get_all_hosted_zones(self, start_marker=None, zone_list=None):
108        """
109        Returns a Python data structure with information about all
110        Hosted Zones defined for the AWS account.
111
112        :param int start_marker: start marker to pass when fetching additional
113            results after a truncated list
114        :param list zone_list: a HostedZones list to prepend to results
115        """
116        params = {}
117        if start_marker:
118            params = {'marker': start_marker}
119        response = self.make_request('GET', '/%s/hostedzone' % self.Version,
120                                     params=params)
121        body = response.read()
122        boto.log.debug(body)
123        if response.status >= 300:
124            raise exception.DNSServerError(response.status,
125                                           response.reason,
126                                           body)
127        e = boto.jsonresponse.Element(list_marker='HostedZones',
128                                      item_marker=('HostedZone',))
129        h = boto.jsonresponse.XmlHandler(e, None)
130        h.parse(body)
131        if zone_list:
132            e['ListHostedZonesResponse']['HostedZones'].extend(zone_list)
133        while 'NextMarker' in e['ListHostedZonesResponse']:
134            next_marker = e['ListHostedZonesResponse']['NextMarker']
135            zone_list = e['ListHostedZonesResponse']['HostedZones']
136            e = self.get_all_hosted_zones(next_marker, zone_list)
137        return e
138
139    def get_hosted_zone(self, hosted_zone_id):
140        """
141        Get detailed information about a particular Hosted Zone.
142
143        :type hosted_zone_id: str
144        :param hosted_zone_id: The unique identifier for the Hosted Zone
145
146        """
147        uri = '/%s/hostedzone/%s' % (self.Version, hosted_zone_id)
148        response = self.make_request('GET', uri)
149        body = response.read()
150        boto.log.debug(body)
151        if response.status >= 300:
152            raise exception.DNSServerError(response.status,
153                                           response.reason,
154                                           body)
155        e = boto.jsonresponse.Element(list_marker='NameServers',
156                                      item_marker=('NameServer',))
157        h = boto.jsonresponse.XmlHandler(e, None)
158        h.parse(body)
159        return e
160
161    def get_hosted_zone_by_name(self, hosted_zone_name):
162        """
163        Get detailed information about a particular Hosted Zone.
164
165        :type hosted_zone_name: str
166        :param hosted_zone_name: The fully qualified domain name for the Hosted
167            Zone
168
169        """
170        if hosted_zone_name[-1] != '.':
171            hosted_zone_name += '.'
172        all_hosted_zones = self.get_all_hosted_zones()
173        for zone in all_hosted_zones['ListHostedZonesResponse']['HostedZones']:
174            # check that they gave us the FQDN for their zone
175            if zone['Name'] == hosted_zone_name:
176                return self.get_hosted_zone(zone['Id'].split('/')[-1])
177
178    def create_hosted_zone(self, domain_name, caller_ref=None, comment='',
179                           private_zone=False, vpc_id=None, vpc_region=None):
180        """
181        Create a new Hosted Zone.  Returns a Python data structure with
182        information about the newly created Hosted Zone.
183
184        :type domain_name: str
185        :param domain_name: The name of the domain. This should be a
186            fully-specified domain, and should end with a final period
187            as the last label indication.  If you omit the final period,
188            Amazon Route 53 assumes the domain is relative to the root.
189            This is the name you have registered with your DNS registrar.
190            It is also the name you will delegate from your registrar to
191            the Amazon Route 53 delegation servers returned in
192            response to this request.A list of strings with the image
193            IDs wanted.
194
195        :type caller_ref: str
196        :param caller_ref: A unique string that identifies the request
197            and that allows failed CreateHostedZone requests to be retried
198            without the risk of executing the operation twice.  If you don't
199            provide a value for this, boto will generate a Type 4 UUID and
200            use that.
201
202        :type comment: str
203        :param comment: Any comments you want to include about the hosted
204            zone.
205
206        :type private_zone: bool
207        :param private_zone: Set True if creating a private hosted zone.
208
209        :type vpc_id: str
210        :param vpc_id: When creating a private hosted zone, the VPC Id to
211            associate to is required.
212
213        :type vpc_region: str
214        :param vpc_id: When creating a private hosted zone, the region of
215            the associated VPC is required.
216
217        """
218        if caller_ref is None:
219            caller_ref = str(uuid.uuid4())
220        if private_zone:
221            params = {'name': domain_name,
222                      'caller_ref': caller_ref,
223                      'comment': comment,
224                      'vpc_id': vpc_id,
225                      'vpc_region': vpc_region,
226                      'xmlns': self.XMLNameSpace}
227            xml_body = HZPXML % params
228        else:
229            params = {'name': domain_name,
230                      'caller_ref': caller_ref,
231                      'comment': comment,
232                      'xmlns': self.XMLNameSpace}
233            xml_body = HZXML % params
234        uri = '/%s/hostedzone' % self.Version
235        response = self.make_request('POST', uri,
236                                     {'Content-Type': 'text/xml'}, xml_body)
237        body = response.read()
238        boto.log.debug(body)
239        if response.status == 201:
240            e = boto.jsonresponse.Element(list_marker='NameServers',
241                                          item_marker=('NameServer',))
242            h = boto.jsonresponse.XmlHandler(e, None)
243            h.parse(body)
244            return e
245        else:
246            raise exception.DNSServerError(response.status,
247                                           response.reason,
248                                           body)
249
250    def delete_hosted_zone(self, hosted_zone_id):
251        """
252        Delete the hosted zone specified by the given id.
253
254        :type hosted_zone_id: str
255        :param hosted_zone_id: The hosted zone's id
256
257        """
258        uri = '/%s/hostedzone/%s' % (self.Version, hosted_zone_id)
259        response = self.make_request('DELETE', uri)
260        body = response.read()
261        boto.log.debug(body)
262        if response.status not in (200, 204):
263            raise exception.DNSServerError(response.status,
264                                           response.reason,
265                                           body)
266        e = boto.jsonresponse.Element()
267        h = boto.jsonresponse.XmlHandler(e, None)
268        h.parse(body)
269        return e
270
271    # Health checks
272
273    POSTHCXMLBody = """<CreateHealthCheckRequest xmlns="%(xmlns)s">
274    <CallerReference>%(caller_ref)s</CallerReference>
275    %(health_check)s
276    </CreateHealthCheckRequest>"""
277
278    def create_health_check(self, health_check, caller_ref=None):
279        """
280        Create a new Health Check
281
282        :type health_check: HealthCheck
283        :param health_check: HealthCheck object
284
285        :type caller_ref: str
286        :param caller_ref: A unique string that identifies the request
287            and that allows failed CreateHealthCheckRequest requests to be retried
288            without the risk of executing the operation twice.  If you don't
289            provide a value for this, boto will generate a Type 4 UUID and
290            use that.
291
292        """
293        if caller_ref is None:
294            caller_ref = str(uuid.uuid4())
295        uri = '/%s/healthcheck' % self.Version
296        params = {'xmlns': self.XMLNameSpace,
297                  'caller_ref': caller_ref,
298                  'health_check': health_check.to_xml()
299                  }
300        xml_body = self.POSTHCXMLBody % params
301        response = self.make_request('POST', uri, {'Content-Type': 'text/xml'}, xml_body)
302        body = response.read()
303        boto.log.debug(body)
304        if response.status == 201:
305            e = boto.jsonresponse.Element()
306            h = boto.jsonresponse.XmlHandler(e, None)
307            h.parse(body)
308            return e
309        else:
310            raise exception.DNSServerError(response.status, response.reason, body)
311
312    def get_list_health_checks(self, maxitems=None, marker=None):
313        """
314        Return a list of health checks
315
316        :type maxitems: int
317        :param maxitems: Maximum number of items to return
318
319        :type marker: str
320        :param marker: marker to get next set of items to list
321
322        """
323
324        params = {}
325        if maxitems is not None:
326            params['maxitems'] = maxitems
327        if marker is not None:
328            params['marker'] = marker
329
330        uri = '/%s/healthcheck' % (self.Version, )
331        response = self.make_request('GET', uri, params=params)
332        body = response.read()
333        boto.log.debug(body)
334        if response.status >= 300:
335            raise exception.DNSServerError(response.status,
336                                           response.reason,
337                                           body)
338        e = boto.jsonresponse.Element(list_marker='HealthChecks',
339                                      item_marker=('HealthCheck',))
340        h = boto.jsonresponse.XmlHandler(e, None)
341        h.parse(body)
342        return e
343
344    def get_checker_ip_ranges(self):
345        """
346        Return a list of Route53 healthcheck IP ranges
347        """
348        uri = '/%s/checkeripranges' % self.Version
349        response = self.make_request('GET', uri)
350        body = response.read()
351        boto.log.debug(body)
352        if response.status >= 300:
353            raise exception.DNSServerError(response.status,
354                                           response.reason,
355                                           body)
356        e = boto.jsonresponse.Element(list_marker='CheckerIpRanges', item_marker=('member',))
357        h = boto.jsonresponse.XmlHandler(e, None)
358        h.parse(body)
359        return e
360
361    def delete_health_check(self, health_check_id):
362        """
363        Delete a health check
364
365        :type health_check_id: str
366        :param health_check_id: ID of the health check to delete
367
368        """
369        uri = '/%s/healthcheck/%s' % (self.Version, health_check_id)
370        response = self.make_request('DELETE', uri)
371        body = response.read()
372        boto.log.debug(body)
373        if response.status not in (200, 204):
374            raise exception.DNSServerError(response.status,
375                                           response.reason,
376                                           body)
377        e = boto.jsonresponse.Element()
378        h = boto.jsonresponse.XmlHandler(e, None)
379        h.parse(body)
380        return e
381
382    # Resource Record Sets
383
384    def get_all_rrsets(self, hosted_zone_id, type=None,
385                       name=None, identifier=None, maxitems=None):
386        """
387        Retrieve the Resource Record Sets defined for this Hosted Zone.
388        Returns the raw XML data returned by the Route53 call.
389
390        :type hosted_zone_id: str
391        :param hosted_zone_id: The unique identifier for the Hosted Zone
392
393        :type type: str
394        :param type: The type of resource record set to begin the record
395            listing from.  Valid choices are:
396
397                * A
398                * AAAA
399                * CNAME
400                * MX
401                * NS
402                * PTR
403                * SOA
404                * SPF
405                * SRV
406                * TXT
407
408            Valid values for weighted resource record sets:
409
410                * A
411                * AAAA
412                * CNAME
413                * TXT
414
415            Valid values for Zone Apex Aliases:
416
417                * A
418                * AAAA
419
420        :type name: str
421        :param name: The first name in the lexicographic ordering of domain
422                     names to be retrieved
423
424        :type identifier: str
425        :param identifier: In a hosted zone that includes weighted resource
426            record sets (multiple resource record sets with the same DNS
427            name and type that are differentiated only by SetIdentifier),
428            if results were truncated for a given DNS name and type,
429            the value of SetIdentifier for the next resource record
430            set that has the current DNS name and type
431
432        :type maxitems: int
433        :param maxitems: The maximum number of records
434
435        """
436        params = {'type': type, 'name': name,
437                  'identifier': identifier, 'maxitems': maxitems}
438        uri = '/%s/hostedzone/%s/rrset' % (self.Version, hosted_zone_id)
439        response = self.make_request('GET', uri, params=params)
440        body = response.read()
441        boto.log.debug(body)
442        if response.status >= 300:
443            raise exception.DNSServerError(response.status,
444                                           response.reason,
445                                           body)
446        rs = ResourceRecordSets(connection=self, hosted_zone_id=hosted_zone_id)
447        h = handler.XmlHandler(rs, self)
448        xml.sax.parseString(body, h)
449        return rs
450
451    def change_rrsets(self, hosted_zone_id, xml_body):
452        """
453        Create or change the authoritative DNS information for this
454        Hosted Zone.
455        Returns a Python data structure with information about the set of
456        changes, including the Change ID.
457
458        :type hosted_zone_id: str
459        :param hosted_zone_id: The unique identifier for the Hosted Zone
460
461        :type xml_body: str
462        :param xml_body: The list of changes to be made, defined in the
463            XML schema defined by the Route53 service.
464
465        """
466        uri = '/%s/hostedzone/%s/rrset' % (self.Version, hosted_zone_id)
467        response = self.make_request('POST', uri,
468                                     {'Content-Type': 'text/xml'},
469                                     xml_body)
470        body = response.read()
471        boto.log.debug(body)
472        if response.status >= 300:
473            raise exception.DNSServerError(response.status,
474                                           response.reason,
475                                           body)
476        e = boto.jsonresponse.Element()
477        h = boto.jsonresponse.XmlHandler(e, None)
478        h.parse(body)
479        return e
480
481    def get_change(self, change_id):
482        """
483        Get information about a proposed set of changes, as submitted
484        by the change_rrsets method.
485        Returns a Python data structure with status information about the
486        changes.
487
488        :type change_id: str
489        :param change_id: The unique identifier for the set of changes.
490            This ID is returned in the response to the change_rrsets method.
491
492        """
493        uri = '/%s/change/%s' % (self.Version, change_id)
494        response = self.make_request('GET', uri)
495        body = response.read()
496        boto.log.debug(body)
497        if response.status >= 300:
498            raise exception.DNSServerError(response.status,
499                                           response.reason,
500                                           body)
501        e = boto.jsonresponse.Element()
502        h = boto.jsonresponse.XmlHandler(e, None)
503        h.parse(body)
504        return e
505
506    def create_zone(self, name, private_zone=False,
507                    vpc_id=None, vpc_region=None):
508        """
509        Create a new Hosted Zone.  Returns a Zone object for the newly
510        created Hosted Zone.
511
512        :type name: str
513        :param name: The name of the domain. This should be a
514            fully-specified domain, and should end with a final period
515            as the last label indication.  If you omit the final period,
516            Amazon Route 53 assumes the domain is relative to the root.
517            This is the name you have registered with your DNS registrar.
518            It is also the name you will delegate from your registrar to
519            the Amazon Route 53 delegation servers returned in
520            response to this request.
521
522        :type private_zone: bool
523        :param private_zone: Set True if creating a private hosted zone.
524
525        :type vpc_id: str
526        :param vpc_id: When creating a private hosted zone, the VPC Id to
527            associate to is required.
528
529        :type vpc_region: str
530        :param vpc_id: When creating a private hosted zone, the region of
531            the associated VPC is required.
532        """
533        zone = self.create_hosted_zone(name, private_zone=private_zone,
534                                       vpc_id=vpc_id, vpc_region=vpc_region)
535        return Zone(self, zone['CreateHostedZoneResponse']['HostedZone'])
536
537    def get_zone(self, name):
538        """
539        Returns a Zone object for the specified Hosted Zone.
540
541        :param name: The name of the domain. This should be a
542            fully-specified domain, and should end with a final period
543            as the last label indication.
544        """
545        name = self._make_qualified(name)
546        for zone in self.get_zones():
547            if name == zone.name:
548                return zone
549
550    def get_zones(self):
551        """
552        Returns a list of Zone objects, one for each of the Hosted
553        Zones defined for the AWS account.
554
555        :rtype: list
556        :returns: A list of Zone objects.
557
558        """
559        zones = self.get_all_hosted_zones()
560        return [Zone(self, zone) for zone in
561                zones['ListHostedZonesResponse']['HostedZones']]
562
563    def _make_qualified(self, value):
564        """
565        Ensure passed domain names end in a period (.) character.
566        This will usually make a domain fully qualified.
567        """
568        if type(value) in [list, tuple, set]:
569            new_list = []
570            for record in value:
571                if record and not record[-1] == '.':
572                    new_list.append("%s." % record)
573                else:
574                    new_list.append(record)
575            return new_list
576        else:
577            value = value.strip()
578            if value and not value[-1] == '.':
579                value = "%s." % value
580            return value
581
582    def _retry_handler(self, response, i, next_sleep):
583        status = None
584        boto.log.debug("Saw HTTP status: %s" % response.status)
585
586        if response.status == 400:
587            code = response.getheader('Code')
588
589            if code:
590                # This is a case where we need to ignore a 400 error, as
591                # Route53 returns this. See
592                # http://docs.aws.amazon.com/Route53/latest/DeveloperGuide/DNSLimitations.html
593                if 'PriorRequestNotComplete' in code:
594                    error = 'PriorRequestNotComplete'
595                elif 'Throttling' in code:
596                    error = 'Throttling'
597                else:
598                    return status
599                msg = "%s, retry attempt %s" % (
600                    error,
601                    i
602                )
603                next_sleep = min(random.random() * (2 ** i),
604                                 boto.config.get('Boto', 'max_retry_delay', 60))
605                i += 1
606                status = (msg, i, next_sleep)
607
608        return status
609