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