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