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