Updates the Shipyard error responses to match the modified structure
for undercloud component error structures. The primary modification is the change from "errorList" to "messageList", and the addition of a error boolean to each messageList record. The docstring for the default error handler has been updated to further explain the relationship between error and info messages and the default error message. Change-Id: If91fee3cc1f59ff07c1b91eee11dc4f3d629bb0c
This commit is contained in:
parent
b4b68c2a54
commit
b98cbf93ee
@ -29,28 +29,65 @@ def get_version_from_request(req):
|
||||
|
||||
|
||||
# Standard error handler
|
||||
def format_resp(req,
|
||||
resp,
|
||||
status_code,
|
||||
message="",
|
||||
reason="",
|
||||
error_type="Unspecified Exception",
|
||||
retry=False,
|
||||
error_list=None):
|
||||
def format_error_resp(req,
|
||||
resp,
|
||||
status_code,
|
||||
message="",
|
||||
reason="",
|
||||
error_type=None,
|
||||
retry=False,
|
||||
error_list=None,
|
||||
info_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 message: Optional error message to include in the body.
|
||||
This should be the summary level of the error
|
||||
message, encompassing an overall result. If
|
||||
no other messages are passed in the error_list,
|
||||
this message will be repeated in a generated
|
||||
message for the output message_list.
|
||||
:param reason: Optional reason code to include in the body
|
||||
:param error_type: If specified, the error type will be used,
|
||||
otherwise, this will be set to
|
||||
'Unspecified Exception'
|
||||
: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
|
||||
:param error_list: optional list of error dictionaries. Minimally,
|
||||
the dictionary will contain the 'message' field,
|
||||
but should also contain 'error': True
|
||||
:param info_list: optional list of info message dictionaries.
|
||||
Minimally, the dictionary needs to contain a
|
||||
'message' field, but should also have a
|
||||
'error': False field.
|
||||
"""
|
||||
|
||||
if error_type is None:
|
||||
error_type = 'Unspecified Exception'
|
||||
|
||||
# since we're handling errors here, if error list is none, set
|
||||
# up a default error item. If we have info items, add them to the
|
||||
# message list as well. In both cases, if the error flag is not
|
||||
# set, set it appropriately.
|
||||
if error_list is None:
|
||||
error_list = [{'message': 'An error ocurred, but was not specified'}]
|
||||
error_list = [{'message': 'An error ocurred, but was not specified',
|
||||
'error': True}]
|
||||
else:
|
||||
for error_item in error_list:
|
||||
if 'error' not in error_item:
|
||||
error_item['error'] = True
|
||||
|
||||
if info_list is None:
|
||||
info_list = []
|
||||
else:
|
||||
for info_item in info_list:
|
||||
if 'error' not in info_item:
|
||||
info_item['error'] = False
|
||||
|
||||
message_list = error_list + info_list
|
||||
|
||||
error_response = {
|
||||
'kind': 'status',
|
||||
'apiVersion': get_version_from_request(req),
|
||||
@ -61,9 +98,10 @@ def format_resp(req,
|
||||
'details': {
|
||||
'errorType': error_type,
|
||||
'errorCount': len(error_list),
|
||||
'errorList': error_list
|
||||
'messageList': message_list
|
||||
},
|
||||
'code': status_code
|
||||
'code': status_code,
|
||||
'retry': retry
|
||||
}
|
||||
|
||||
resp.body = json.dumps(error_response, default=str)
|
||||
@ -75,14 +113,15 @@ def default_error_serializer(req, resp, exception):
|
||||
"""
|
||||
Writes the default error message body, when we don't handle it otherwise
|
||||
"""
|
||||
format_resp(
|
||||
format_error_resp(
|
||||
req,
|
||||
resp,
|
||||
status_code=exception.status,
|
||||
message=exception.description,
|
||||
reason=exception.title,
|
||||
error_type=exception.__class__.__name__,
|
||||
error_list=[{'message': exception.description}]
|
||||
error_list=[{'message': exception.description, 'error': True}],
|
||||
info_list=None
|
||||
)
|
||||
|
||||
|
||||
@ -98,7 +137,7 @@ def default_exception_handler(ex, req, resp, params):
|
||||
# take care of the uncaught stuff
|
||||
exc_string = traceback.format_exc()
|
||||
logging.error('Unhanded Exception being handled: \n%s', exc_string)
|
||||
format_resp(
|
||||
format_error_resp(
|
||||
req,
|
||||
resp,
|
||||
falcon.HTTP_500,
|
||||
@ -113,34 +152,63 @@ class AppError(Exception):
|
||||
Base error containing enough information to make a shipyard formatted error
|
||||
"""
|
||||
|
||||
title = 'Internal Server Error'
|
||||
status = falcon.HTTP_500
|
||||
|
||||
def __init__(self,
|
||||
title='Internal Server Error',
|
||||
title=None,
|
||||
description=None,
|
||||
error_list=None,
|
||||
status=falcon.HTTP_500,
|
||||
retry=False):
|
||||
info_list=None,
|
||||
status=None,
|
||||
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 error_list: A list of errors to be included in output
|
||||
messages list
|
||||
:param info_list: A list of informational messages to be
|
||||
included in the output messages list
|
||||
:param retry: Optional retry directive for the consumer
|
||||
"""
|
||||
self.title = title
|
||||
|
||||
if title is None:
|
||||
self.title = self.__class__.title
|
||||
else:
|
||||
self.title = title
|
||||
|
||||
if status is None:
|
||||
self.status = self.__class__.status
|
||||
else:
|
||||
self.status = status
|
||||
|
||||
self.description = description
|
||||
self.error_list = massage_error_list(error_list, description)
|
||||
self.status = status
|
||||
self.info_list = info_list
|
||||
self.retry = retry
|
||||
super().__init__(AppError._gen_ex_message(title, description))
|
||||
|
||||
@staticmethod
|
||||
def _gen_ex_message(title, description):
|
||||
ttl = title or 'Exception'
|
||||
dsc = description or 'No additional decsription'
|
||||
return '{} : {}'.format(ttl, dsc)
|
||||
|
||||
@staticmethod
|
||||
def handle(ex, req, resp, params):
|
||||
format_resp(
|
||||
"""
|
||||
The handler used for app errors and child classes
|
||||
"""
|
||||
format_error_resp(
|
||||
req,
|
||||
resp,
|
||||
ex.status,
|
||||
message=ex.title,
|
||||
reason=ex.description,
|
||||
error_list=ex.error_list,
|
||||
info_list=ex.info_list,
|
||||
error_type=ex.__class__.__name__,
|
||||
retry=ex.retry)
|
||||
|
||||
@ -150,34 +218,17 @@ 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
|
||||
)
|
||||
title = "Error response from Airflow"
|
||||
status = falcon.HTTP_400
|
||||
|
||||
|
||||
class DatabaseError(AppError):
|
||||
"""
|
||||
An error to handle general api errors.
|
||||
An app error specific to database processing.
|
||||
"""
|
||||
|
||||
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
|
||||
)
|
||||
title = 'Database Access Error'
|
||||
status = falcon.HTTP_500
|
||||
|
||||
|
||||
class ApiError(AppError):
|
||||
@ -185,19 +236,8 @@ 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
|
||||
)
|
||||
title = 'Api Error'
|
||||
status = falcon.HTTP_400
|
||||
|
||||
|
||||
class InvalidFormatError(AppError):
|
||||
@ -205,15 +245,8 @@ 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
|
||||
)
|
||||
title = 'Validation Failure'
|
||||
status = falcon.HTTP_400
|
||||
|
||||
|
||||
def massage_error_list(error_list, placeholder_description):
|
||||
@ -223,9 +256,11 @@ def massage_error_list(error_list, placeholder_description):
|
||||
output_error_list = []
|
||||
if error_list:
|
||||
for error in error_list:
|
||||
if not error['message']:
|
||||
output_error_list.append({'message': error})
|
||||
if not error.get('message'):
|
||||
output_error_list.append({'message': error, 'error': True})
|
||||
else:
|
||||
if 'error' not in error:
|
||||
error['error'] = True
|
||||
output_error_list.append(error)
|
||||
if not output_error_list:
|
||||
output_error_list.append({'message': placeholder_description})
|
||||
|
70
tests/unit/test_errors.py
Normal file
70
tests/unit/test_errors.py
Normal file
@ -0,0 +1,70 @@
|
||||
# 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.
|
||||
from shipyard_airflow import errors
|
||||
|
||||
|
||||
def test_app_error():
|
||||
"""
|
||||
test app error for status and title overrides
|
||||
"""
|
||||
|
||||
err = errors.AppError()
|
||||
assert err.status == errors.AppError.status
|
||||
assert err.title == errors.AppError.title
|
||||
|
||||
err = errors.AppError(title='Unfortunate News Everyone', status=999)
|
||||
assert err.status == 999
|
||||
assert err.title == 'Unfortunate News Everyone'
|
||||
|
||||
|
||||
def test_api_error():
|
||||
"""
|
||||
test api error
|
||||
"""
|
||||
|
||||
err = errors.ApiError()
|
||||
assert err.status == errors.ApiError.status
|
||||
assert err.title == errors.ApiError.title
|
||||
|
||||
err = errors.ApiError(title='It was the worst of times', status=1)
|
||||
assert err.title == 'It was the worst of times'
|
||||
assert err.status == 1
|
||||
|
||||
|
||||
def test_database_error():
|
||||
"""
|
||||
test database error
|
||||
"""
|
||||
|
||||
err = errors.DatabaseError()
|
||||
assert err.status == errors.DatabaseError.status
|
||||
assert err.title == errors.DatabaseError.title
|
||||
|
||||
err = errors.DatabaseError(title='Stuff Happens', status=1990)
|
||||
assert err.status == 1990
|
||||
assert err.title == 'Stuff Happens'
|
||||
|
||||
|
||||
def test_airflow_error():
|
||||
"""
|
||||
test airflow error
|
||||
"""
|
||||
|
||||
err = errors.AirflowError()
|
||||
assert err.status == errors.AirflowError.status
|
||||
assert err.title == errors.AirflowError.title
|
||||
|
||||
err = errors.AirflowError(title='cron save us', status=500)
|
||||
assert err.status == 500
|
||||
assert err.title == 'cron save us'
|
Loading…
x
Reference in New Issue
Block a user