1# Copyright (c) 2006-2010 Mitch Garnaat http://garnaat.org/ 2# Copyright (c) 2010, Eucalyptus Systems, Inc. 3# All rights reserved. 4# 5# Permission is hereby granted, free of charge, to any person obtaining a 6# copy of this software and associated documentation files (the 7# "Software"), to deal in the Software without restriction, including 8# without limitation the rights to use, copy, modify, merge, publish, dis- 9# tribute, sublicense, and/or sell copies of the Software, and to permit 10# persons to whom the Software is furnished to do so, subject to the fol- 11# lowing conditions: 12# 13# The above copyright notice and this permission notice shall be included 14# in all copies or substantial portions of the Software. 15# 16# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 17# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL- 18# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS 22# IN THE SOFTWARE. 23 24""" 25Exception classes - Subclassing allows you to check for specific errors 26""" 27import base64 28import xml.sax 29 30import boto 31 32from boto import handler 33from boto.compat import json, StandardError 34from boto.resultset import ResultSet 35 36 37class BotoClientError(StandardError): 38 """ 39 General Boto Client error (error accessing AWS) 40 """ 41 def __init__(self, reason, *args): 42 super(BotoClientError, self).__init__(reason, *args) 43 self.reason = reason 44 45 def __repr__(self): 46 return 'BotoClientError: %s' % self.reason 47 48 def __str__(self): 49 return 'BotoClientError: %s' % self.reason 50 51 52class SDBPersistenceError(StandardError): 53 pass 54 55 56class StoragePermissionsError(BotoClientError): 57 """ 58 Permissions error when accessing a bucket or key on a storage service. 59 """ 60 pass 61 62 63class S3PermissionsError(StoragePermissionsError): 64 """ 65 Permissions error when accessing a bucket or key on S3. 66 """ 67 pass 68 69 70class GSPermissionsError(StoragePermissionsError): 71 """ 72 Permissions error when accessing a bucket or key on GS. 73 """ 74 pass 75 76 77class BotoServerError(StandardError): 78 def __init__(self, status, reason, body=None, *args): 79 super(BotoServerError, self).__init__(status, reason, body, *args) 80 self.status = status 81 self.reason = reason 82 self.body = body or '' 83 self.request_id = None 84 self.error_code = None 85 self._error_message = None 86 self.message = '' 87 self.box_usage = None 88 89 if isinstance(self.body, bytes): 90 try: 91 self.body = self.body.decode('utf-8') 92 except UnicodeDecodeError: 93 boto.log.debug('Unable to decode body from bytes!') 94 95 # Attempt to parse the error response. If body isn't present, 96 # then just ignore the error response. 97 if self.body: 98 # Check if it looks like a ``dict``. 99 if hasattr(self.body, 'items'): 100 # It's not a string, so trying to parse it will fail. 101 # But since it's data, we can work with that. 102 self.request_id = self.body.get('RequestId', None) 103 104 if 'Error' in self.body: 105 # XML-style 106 error = self.body.get('Error', {}) 107 self.error_code = error.get('Code', None) 108 self.message = error.get('Message', None) 109 else: 110 # JSON-style. 111 self.message = self.body.get('message', None) 112 else: 113 try: 114 h = handler.XmlHandlerWrapper(self, self) 115 h.parseString(self.body) 116 except (TypeError, xml.sax.SAXParseException): 117 # What if it's JSON? Let's try that. 118 try: 119 parsed = json.loads(self.body) 120 121 if 'RequestId' in parsed: 122 self.request_id = parsed['RequestId'] 123 if 'Error' in parsed: 124 if 'Code' in parsed['Error']: 125 self.error_code = parsed['Error']['Code'] 126 if 'Message' in parsed['Error']: 127 self.message = parsed['Error']['Message'] 128 129 except (TypeError, ValueError): 130 # Remove unparsable message body so we don't include garbage 131 # in exception. But first, save self.body in self.error_message 132 # because occasionally we get error messages from Eucalyptus 133 # that are just text strings that we want to preserve. 134 self.message = self.body 135 self.body = None 136 137 def __getattr__(self, name): 138 if name == 'error_message': 139 return self.message 140 if name == 'code': 141 return self.error_code 142 raise AttributeError 143 144 def __setattr__(self, name, value): 145 if name == 'error_message': 146 self.message = value 147 else: 148 super(BotoServerError, self).__setattr__(name, value) 149 150 def __repr__(self): 151 return '%s: %s %s\n%s' % (self.__class__.__name__, 152 self.status, self.reason, self.body) 153 154 def __str__(self): 155 return '%s: %s %s\n%s' % (self.__class__.__name__, 156 self.status, self.reason, self.body) 157 158 def startElement(self, name, attrs, connection): 159 pass 160 161 def endElement(self, name, value, connection): 162 if name in ('RequestId', 'RequestID'): 163 self.request_id = value 164 elif name == 'Code': 165 self.error_code = value 166 elif name == 'Message': 167 self.message = value 168 elif name == 'BoxUsage': 169 self.box_usage = value 170 return None 171 172 def _cleanupParsedProperties(self): 173 self.request_id = None 174 self.error_code = None 175 self.message = None 176 self.box_usage = None 177 178 179class ConsoleOutput(object): 180 def __init__(self, parent=None): 181 self.parent = parent 182 self.instance_id = None 183 self.timestamp = None 184 self.comment = None 185 self.output = None 186 187 def startElement(self, name, attrs, connection): 188 return None 189 190 def endElement(self, name, value, connection): 191 if name == 'instanceId': 192 self.instance_id = value 193 elif name == 'output': 194 self.output = base64.b64decode(value) 195 else: 196 setattr(self, name, value) 197 198 199class StorageCreateError(BotoServerError): 200 """ 201 Error creating a bucket or key on a storage service. 202 """ 203 def __init__(self, status, reason, body=None): 204 self.bucket = None 205 super(StorageCreateError, self).__init__(status, reason, body) 206 207 def endElement(self, name, value, connection): 208 if name == 'BucketName': 209 self.bucket = value 210 else: 211 return super(StorageCreateError, self).endElement(name, value, connection) 212 213 214class S3CreateError(StorageCreateError): 215 """ 216 Error creating a bucket or key on S3. 217 """ 218 pass 219 220 221class GSCreateError(StorageCreateError): 222 """ 223 Error creating a bucket or key on GS. 224 """ 225 pass 226 227 228class StorageCopyError(BotoServerError): 229 """ 230 Error copying a key on a storage service. 231 """ 232 pass 233 234 235class S3CopyError(StorageCopyError): 236 """ 237 Error copying a key on S3. 238 """ 239 pass 240 241 242class GSCopyError(StorageCopyError): 243 """ 244 Error copying a key on GS. 245 """ 246 pass 247 248 249class SQSError(BotoServerError): 250 """ 251 General Error on Simple Queue Service. 252 """ 253 def __init__(self, status, reason, body=None): 254 self.detail = None 255 self.type = None 256 super(SQSError, self).__init__(status, reason, body) 257 258 def startElement(self, name, attrs, connection): 259 return super(SQSError, self).startElement(name, attrs, connection) 260 261 def endElement(self, name, value, connection): 262 if name == 'Detail': 263 self.detail = value 264 elif name == 'Type': 265 self.type = value 266 else: 267 return super(SQSError, self).endElement(name, value, connection) 268 269 def _cleanupParsedProperties(self): 270 super(SQSError, self)._cleanupParsedProperties() 271 for p in ('detail', 'type'): 272 setattr(self, p, None) 273 274 275class SQSDecodeError(BotoClientError): 276 """ 277 Error when decoding an SQS message. 278 """ 279 def __init__(self, reason, message): 280 super(SQSDecodeError, self).__init__(reason, message) 281 self.message = message 282 283 def __repr__(self): 284 return 'SQSDecodeError: %s' % self.reason 285 286 def __str__(self): 287 return 'SQSDecodeError: %s' % self.reason 288 289 290class StorageResponseError(BotoServerError): 291 """ 292 Error in response from a storage service. 293 """ 294 def __init__(self, status, reason, body=None): 295 self.resource = None 296 super(StorageResponseError, self).__init__(status, reason, body) 297 298 def startElement(self, name, attrs, connection): 299 return super(StorageResponseError, self).startElement( 300 name, attrs, connection) 301 302 def endElement(self, name, value, connection): 303 if name == 'Resource': 304 self.resource = value 305 else: 306 return super(StorageResponseError, self).endElement( 307 name, value, connection) 308 309 def _cleanupParsedProperties(self): 310 super(StorageResponseError, self)._cleanupParsedProperties() 311 for p in ('resource'): 312 setattr(self, p, None) 313 314 315class S3ResponseError(StorageResponseError): 316 """ 317 Error in response from S3. 318 """ 319 pass 320 321 322class GSResponseError(StorageResponseError): 323 """ 324 Error in response from GS. 325 """ 326 pass 327 328 329class EC2ResponseError(BotoServerError): 330 """ 331 Error in response from EC2. 332 """ 333 def __init__(self, status, reason, body=None): 334 self.errors = None 335 self._errorResultSet = [] 336 super(EC2ResponseError, self).__init__(status, reason, body) 337 self.errors = [ 338 (e.error_code, e.error_message) for e in self._errorResultSet] 339 if len(self.errors): 340 self.error_code, self.error_message = self.errors[0] 341 342 def startElement(self, name, attrs, connection): 343 if name == 'Errors': 344 self._errorResultSet = ResultSet([('Error', _EC2Error)]) 345 return self._errorResultSet 346 else: 347 return None 348 349 def endElement(self, name, value, connection): 350 if name == 'RequestID': 351 self.request_id = value 352 else: 353 return None # don't call subclass here 354 355 def _cleanupParsedProperties(self): 356 super(EC2ResponseError, self)._cleanupParsedProperties() 357 self._errorResultSet = [] 358 for p in ('errors'): 359 setattr(self, p, None) 360 361 362class JSONResponseError(BotoServerError): 363 """ 364 This exception expects the fully parsed and decoded JSON response 365 body to be passed as the body parameter. 366 367 :ivar status: The HTTP status code. 368 :ivar reason: The HTTP reason message. 369 :ivar body: The Python dict that represents the decoded JSON 370 response body. 371 :ivar error_message: The full description of the AWS error encountered. 372 :ivar error_code: A short string that identifies the AWS error 373 (e.g. ConditionalCheckFailedException) 374 """ 375 def __init__(self, status, reason, body=None, *args): 376 self.status = status 377 self.reason = reason 378 self.body = body 379 if self.body: 380 self.error_message = self.body.get('message', None) 381 self.error_code = self.body.get('__type', None) 382 if self.error_code: 383 self.error_code = self.error_code.split('#')[-1] 384 385 386class DynamoDBResponseError(JSONResponseError): 387 pass 388 389 390class SWFResponseError(JSONResponseError): 391 pass 392 393 394class EmrResponseError(BotoServerError): 395 """ 396 Error in response from EMR 397 """ 398 pass 399 400 401class _EC2Error(object): 402 def __init__(self, connection=None): 403 self.connection = connection 404 self.error_code = None 405 self.error_message = None 406 407 def startElement(self, name, attrs, connection): 408 return None 409 410 def endElement(self, name, value, connection): 411 if name == 'Code': 412 self.error_code = value 413 elif name == 'Message': 414 self.error_message = value 415 else: 416 return None 417 418 419class SDBResponseError(BotoServerError): 420 """ 421 Error in responses from SDB. 422 """ 423 pass 424 425 426class AWSConnectionError(BotoClientError): 427 """ 428 General error connecting to Amazon Web Services. 429 """ 430 pass 431 432 433class StorageDataError(BotoClientError): 434 """ 435 Error receiving data from a storage service. 436 """ 437 pass 438 439 440class S3DataError(StorageDataError): 441 """ 442 Error receiving data from S3. 443 """ 444 pass 445 446 447class GSDataError(StorageDataError): 448 """ 449 Error receiving data from GS. 450 """ 451 pass 452 453 454class InvalidUriError(Exception): 455 """Exception raised when URI is invalid.""" 456 457 def __init__(self, message): 458 super(InvalidUriError, self).__init__(message) 459 self.message = message 460 461 462class InvalidAclError(Exception): 463 """Exception raised when ACL XML is invalid.""" 464 465 def __init__(self, message): 466 super(InvalidAclError, self).__init__(message) 467 self.message = message 468 469 470class InvalidCorsError(Exception): 471 """Exception raised when CORS XML is invalid.""" 472 473 def __init__(self, message): 474 super(InvalidCorsError, self).__init__(message) 475 self.message = message 476 477 478class NoAuthHandlerFound(Exception): 479 """Is raised when no auth handlers were found ready to authenticate.""" 480 pass 481 482 483class InvalidLifecycleConfigError(Exception): 484 """Exception raised when GCS lifecycle configuration XML is invalid.""" 485 486 def __init__(self, message): 487 super(InvalidLifecycleConfigError, self).__init__(message) 488 self.message = message 489 490 491# Enum class for resumable upload failure disposition. 492class ResumableTransferDisposition(object): 493 # START_OVER means an attempt to resume an existing transfer failed, 494 # and a new resumable upload should be attempted (without delay). 495 START_OVER = 'START_OVER' 496 497 # WAIT_BEFORE_RETRY means the resumable transfer failed but that it can 498 # be retried after a time delay within the current process. 499 WAIT_BEFORE_RETRY = 'WAIT_BEFORE_RETRY' 500 501 # ABORT_CUR_PROCESS means the resumable transfer failed and that 502 # delaying/retrying within the current process will not help. If 503 # resumable transfer included a state tracker file the upload can be 504 # retried again later, in another process (e.g., a later run of gsutil). 505 ABORT_CUR_PROCESS = 'ABORT_CUR_PROCESS' 506 507 # ABORT means the resumable transfer failed in a way that it does not 508 # make sense to continue in the current process, and further that the 509 # current tracker ID should not be preserved (in a tracker file if one 510 # was specified at resumable upload start time). If the user tries again 511 # later (e.g., a separate run of gsutil) it will get a new resumable 512 # upload ID. 513 ABORT = 'ABORT' 514 515 516class ResumableUploadException(Exception): 517 """ 518 Exception raised for various resumable upload problems. 519 520 self.disposition is of type ResumableTransferDisposition. 521 """ 522 523 def __init__(self, message, disposition): 524 super(ResumableUploadException, self).__init__(message, disposition) 525 self.message = message 526 self.disposition = disposition 527 528 def __repr__(self): 529 return 'ResumableUploadException("%s", %s)' % ( 530 self.message, self.disposition) 531 532 533class ResumableDownloadException(Exception): 534 """ 535 Exception raised for various resumable download problems. 536 537 self.disposition is of type ResumableTransferDisposition. 538 """ 539 540 def __init__(self, message, disposition): 541 super(ResumableDownloadException, self).__init__(message, disposition) 542 self.message = message 543 self.disposition = disposition 544 545 def __repr__(self): 546 return 'ResumableDownloadException("%s", %s)' % ( 547 self.message, self.disposition) 548 549 550class TooManyRecordsException(Exception): 551 """ 552 Exception raised when a search of Route53 records returns more 553 records than requested. 554 """ 555 556 def __init__(self, message): 557 super(TooManyRecordsException, self).__init__(message) 558 self.message = message 559 560 561class PleaseRetryException(Exception): 562 """ 563 Indicates a request should be retried. 564 """ 565 def __init__(self, message, response=None): 566 self.message = message 567 self.response = response 568 569 def __repr__(self): 570 return 'PleaseRetryException("%s", %s)' % ( 571 self.message, 572 self.response 573 ) 574