shipyard/shipyard_airflow/errors.py

225 lines
6.7 KiB
Python

# Copyright 2017 AT&T Intellectual Property. All other rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import json
import logging
import traceback
import falcon
def get_version_from_request(req):
"""
Attempt to extract the api version string
"""
for part in req.path.split('/'):
if '.' in part and part.startswith('v'):
return part
return 'N/A'
# Standard error handler
def format_resp(req,
resp,
status_code,
message="",
reason="",
error_type="Unspecified Exception",
retry=False,
error_list=None):
"""
Write a error message body and throw a Falcon exception to trigger
an HTTP status
:param req: Falcon request object
:param resp: Falcon response object to update
:param status_code: Falcon status_code constant
:param message: Optional error message to include in the body
:param reason: Optional reason code to include in the body
:param retry: Optional flag whether client should retry the operation.
:param error_list: option list of errors
Can ignore if we rely solely on 4XX vs 5xx status codes
"""
if error_list is None:
error_list = [{'message': 'An error ocurred, but was not specified'}]
error_response = {
'kind': 'status',
'apiVersion': get_version_from_request(req),
'metadata': {},
'status': 'Failure',
'message': message,
'reason': reason,
'details': {
'errorType': error_type,
'errorCount': len(error_list),
'errorList': error_list
},
'code': status_code
}
resp.body = json.dumps(error_response, default=str)
resp.content_type = 'application/json'
resp.status = status_code
def default_error_serializer(req, resp, exception):
"""
Writes the default error message body, when we don't handle it otherwise
"""
format_resp(
req,
resp,
status_code=exception.status,
message=exception.description,
reason=exception.title,
error_type=exception.__class__.__name__,
error_list=[{'message': exception.description}]
)
def default_exception_handler(ex, req, resp, params):
"""
Catch-all execption handler for standardized output.
If this is a standard falcon HTTPError, rethrow it for handling
"""
if isinstance(ex, falcon.HTTPError):
# allow the falcon http errors to bubble up and get handled
raise ex
else:
# take care of the uncaught stuff
exc_string = traceback.format_exc()
logging.error('Unhanded Exception being handled: \n%s', exc_string)
format_resp(
req,
resp,
falcon.HTTP_500,
error_type=ex.__class__.__name__,
message="Unhandled Exception raised: %s" % str(ex),
retry=True
)
class AppError(Exception):
"""
Base error containing enough information to make a shipyard formatted error
"""
def __init__(self,
title='Internal Server Error',
description=None,
error_list=None,
status=falcon.HTTP_500,
retry=False):
"""
:param description: The internal error description
:param error_list: The list of errors
:param status: The desired falcon HTTP resposne code
:param title: The title of the error message
:param retry: Optional retry directive for the consumer
"""
self.title = title
self.description = description
self.error_list = massage_error_list(error_list, description)
self.status = status
self.retry = retry
@staticmethod
def handle(ex, req, resp, params):
format_resp(
req,
resp,
ex.status,
message=ex.title,
reason=ex.description,
error_list=ex.error_list,
error_type=ex.__class__.__name__,
retry=ex.retry)
class AirflowError(AppError):
"""
An error to handle errors returned by the Airflow API
"""
def __init__(self, description=None, error_list=None):
super().__init__(
title='Error response from Airflow',
description=description,
error_list=error_list,
status=falcon.HTTP_400,
retry=False
)
class DatabaseError(AppError):
"""
An error to handle general api errors.
"""
def __init__(self,
description=None,
error_list=None,
status=falcon.HTTP_500,
title='Database Access Error',
retry=False):
super().__init__(
status=status,
title=title,
description=description,
error_list=error_list,
retry=retry
)
class ApiError(AppError):
"""
An error to handle general api errors.
"""
def __init__(self,
description="",
error_list=None,
status=falcon.HTTP_400,
title="",
retry=False):
super().__init__(
status=status,
title=title,
description=description,
error_list=error_list,
retry=retry
)
class InvalidFormatError(AppError):
"""
An exception to cover invalid input formatting
"""
def __init__(self, title, description="Not Specified", error_list=None):
super().__init__(
title=title,
description='Validation has failed',
error_list=error_list,
status=falcon.HTTP_400,
retry=False
)
def massage_error_list(error_list, placeholder_description):
"""
Returns a best-effort attempt to make a nice error list
"""
output_error_list = []
if error_list:
for error in error_list:
if not error['message']:
output_error_list.append({'message': error})
else:
output_error_list.append(error)
if not output_error_list:
output_error_list.append({'message': placeholder_description})
return output_error_list