# 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 falcon from oslo_log import log as logging import six import yaml LOG = logging.getLogger(__name__) 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' def format_error_resp(req, resp, status_code=falcon.HTTP_500, message="", reason=None, error_type=None, error_list=None, info_list=None): """Generate 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. 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 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. """ error_type = error_type or 'Unspecified Exception' reason = reason or 'Unspecified' # 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 not error_list: error_list = [{'message': message, 'error': True}] else: for error_item in error_list: if 'error' not in error_item: error_item['error'] = True if not info_list: 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), 'metadata': {}, 'status': 'Failure', 'message': message, 'reason': reason, 'details': { 'errorType': error_type, 'errorCount': len(error_list), 'messageList': message_list }, 'code': status_code, # TODO(fmontei): Make this class-specific later. For now, retry # is set to True only for internal server errors. 'retry': True if status_code is falcon.HTTP_500 else False } resp.status = status_code resp.body = yaml.safe_dump(error_response) def default_exception_handler(ex, req, resp, params): """Catch-all exception handler for standardized output. If this is a standard falcon HTTPError, rethrow it for handling by ``default_exception_serializer`` below. """ if isinstance(ex, falcon.HTTPError): # Allow the falcon HTTP errors to bubble up and get handled. raise ex elif isinstance(ex, DeckhandException): status_code = (getattr(falcon, 'HTTP_%d' % ex.code, falcon.HTTP_500) if hasattr(ex, 'code') else falcon.HTTP_500) format_error_resp( req, resp, status_code=status_code, message=ex.message, error_type=ex.__class__.__name__, error_list=getattr(ex, 'error_list', None), reason=getattr(ex, 'reason', None) ) else: # Take care of the uncaught stuff. format_error_resp( req, resp, error_type=ex.__class__.__name__, message="Unhandled Exception raised: %s" % six.text_type(ex) ) def default_exception_serializer(req, resp, exception): """Serializes instances of :class:`falcon.HTTPError` into YAML format and formats the error body so it adheres to the Airship error formatting standard. """ format_error_resp( req, resp, status_code=exception.status, # TODO(fmontei): Provide an overall error message instead. message=exception.description, error_type=exception.__class__.__name__, error_list=getattr(exception, 'error_list', None), reason=getattr(exception, 'reason', None) ) class DeckhandException(Exception): """Base Deckhand Exception To correctly use this class, inherit from it and define a 'msg_fmt' property. That msg_fmt will get printf'd with the keyword arguments provided to the constructor. """ msg_fmt = "An unknown exception occurred" def __init__(self, message=None, code=500, **kwargs): kwargs.setdefault('code', code) if not message: try: message = self.msg_fmt % kwargs except Exception: message = self.msg_fmt self.message = message self.reason = kwargs.pop('reason', None) error_list = kwargs.pop('error_list', []) self.error_list = [] for error in error_list: if isinstance(error, str): error = {'message': error, 'error': True} else: error = error.format_message() self.error_list.append(error) super(DeckhandException, self).__init__(message) def format_message(self): return self.args[0] class InvalidDocumentFormat(DeckhandException): """Schema validations failed for the provided document(s). **Troubleshoot:** """ msg_fmt = ("The provided documents failed schema validation") code = 400 class InvalidDocumentLayer(DeckhandException): """The document layer is invalid. **Troubleshoot:** * Check that the document layer is contained in the layerOrder in the registered LayeringPolicy in the system. """ msg_fmt = ("Invalid layer '%(document_layer)s' for document " "[%(document_schema)s] %(document_name)s was not found in " "layerOrder: %(layer_order)s for provided LayeringPolicy: " "%(layering_policy_name)s") code = 400 class InvalidDocumentParent(DeckhandException): """The document parent is invalid. **Troubleshoot:** * Check that the document `schema` and parent `schema` match. * Check that the document layer is lower-order than the parent layer. """ msg_fmt = ("The document parent [%(parent_schema)s] %(parent_name)s is " "invalid for document [%(document_schema)s] %(document_name)s. " "Reason: %(reason)s") code = 400 class IndeterminateDocumentParent(DeckhandException): """More than one parent document was found for a document. **Troubleshoot:** """ msg_fmt = ("Too many parent documents found for document [%(schema)s, " "%(layer)s] %(name)s. Found: %(found)s. Expected: 1") code = 400 class SubstitutionDependencyCycle(DeckhandException): """An illegal substitution depdencency cycle was detected. **Troubleshoot:** * Check that there is no two-way substitution dependency between documents. """ msg_fmt = ('Cannot determine substitution order as a dependency ' 'cycle exists for the following documents: %(cycle)s') code = 400 class MissingDocumentKey(DeckhandException): """Either the parent or child document data is missing the action path used for layering. **Troubleshoot:** * Check that the action path exists in the data section for both child and parent documents being layered together. * Note that previous delete layering actions can affect future layering actions by removing a path needed by a future layering action. * Note that substitutions that substitute in lists or objects into the rendered data for a document can also complicate debugging this issue. """ msg_fmt = ("Missing action path in %(action)s needed for layering from " "either the data section of the parent [%(parent_schema)s, " "%(parent_layer)s] %(parent_name)s or child [%(child_schema)s, " "%(child_layer)s] %(child_name)s document") code = 400 class MissingDocumentPattern(DeckhandException): """'Pattern' is not None and data[jsonpath] doesn't exist. **Troubleshoot:** * Check that the destination document's data section contains the pattern specified under `substitutions.dest.pattern` in its data section at `substitutions.dest.path`. """ msg_fmt = ("The destination document's `data` section is missing the " "pattern %(pattern)s specified under " "`substitutions.dest.pattern` at path %(jsonpath)s, specified " "under `substitutions.dest.path`") code = 400 class InvalidDocumentReplacement(DeckhandException): """The document replacement is invalid. **Troubleshoot:** * Check that the replacement document has the same ``schema`` and ``metadata.name`` as the document it replaces. * Check that the document with ``replacement: true`` has a parent. * Check that the document replacement isn't being replaced by another document. Only one level of replacement is permitted. """ msg_fmt = ("Replacement document [%(schema)s, %(layer)s] %(name)s is " "invalid. Reason: %(reason)s") code = 400 class UnsupportedActionMethod(DeckhandException): """The action is not in the list of supported methods. **Troubleshoot:** """ msg_fmt = ("Method in %(actions)s is invalid for document %(document)s") code = 400 class RevisionTagBadFormat(DeckhandException): """The tag data is neither None nor dictionary. **Troubleshoot:** """ msg_fmt = ("The requested tag data %(data)s must either be null or " "dictionary") code = 400 class SubstitutionSourceDataNotFound(DeckhandException): """Required substitution source secret was not found in the substitution source document at the path ``metadata.substitutions.[*].src.path`` in the destination document. **Troubleshoot:** * Ensure that the missing source secret exists at the ``src.path`` specified under the given substitution in the destination document and that the ``src.path`` itself exists in the source document. """ msg_fmt = ( "Required substitution source secret was not found at path " "%(src_path)s in source document [%(src_schema)s, %(src_layer)s] " "%(src_name)s which is referenced by destination document " "[%(dest_schema)s, %(dest_layer)s] %(dest_name)s under its " "`metadata.substitutions`") code = 400 class EncryptionSourceNotFound(DeckhandException): """Required encryption source reference was not found. **Troubleshoot:** * Ensure that the secret reference exists among the encryption sources. """ msg_fmt = ( "Required encryption source reference could not be resolved into a " "secret because it was not found among encryption sources. Ref: " "%(secret_ref)s. Referenced by: [%(schema)s, %(layer)s] %(name)s") code = 400 # Indicates bad data was passed in, causing a lookup to fail. class DocumentNotFound(DeckhandException): """The requested document could not be found. **Troubleshoot:** """ msg_fmt = ("The requested document using filters: %(filters)s was not " "found") code = 404 class RevisionNotFound(DeckhandException): """The revision cannot be found or doesn't exist. **Troubleshoot:** """ msg_fmt = "The requested revision=%(revision_id)s was not found" code = 404 class RevisionTagNotFound(DeckhandException): """The tag for the revision id was not found. **Troubleshoot:** """ msg_fmt = ("The requested tag '%(tag)s' for revision %(revision)s was " "not found") code = 404 class ValidationNotFound(DeckhandException): """The requested validation was not found. **Troubleshoot:** """ msg_fmt = ("The requested validation entry %(entry_id)s was not found " "for validation name %(validation_name)s and revision ID " "%(revision_id)s") code = 404 class DuplicateDocumentExists(DeckhandException): """A document attempted to be put into a bucket where another document with the same schema and metadata.name already exist. **Troubleshoot:** """ msg_fmt = ("Document [%(schema)s, %(layer)s] %(name)s already exists in " "bucket: %(bucket)s") code = 409 class SingletonDocumentConflict(DeckhandException): """A singleton document already exist within the system. **Troubleshoot:** """ msg_fmt = ("A singleton document [%(schema)s, %(layer)s] %(name)s already " "exists in the system. The new document(s) %(conflict)s cannot " "be created. To create a document with a new name, delete the " "current one first") code = 409 class LayeringPolicyNotFound(DeckhandException): """Required LayeringPolicy was not found for layering. **Troubleshoot:** """ msg_fmt = ("Required LayeringPolicy was not found for layering") code = 409 class SubstitutionSourceNotFound(DeckhandException): """Required substitution source document was not found. **Troubleshoot:** * Ensure that the missing source document being referenced exists in the system or was passed to the layering module. """ msg_fmt = ( "Required substitution source document [%(src_schema)s] %(src_name)s " "was not found, yet is referenced by [%(document_schema)s] " "%(document_name)s") code = 409 class PolicyNotAuthorized(DeckhandException): """The policy action is not found in the list of registered rules. **Troubleshoot:** """ msg_fmt = "Policy doesn't allow %(action)s to be performed" code = 403 class BarbicanClientException(DeckhandException): """A client-side 4xx error occurred with Barbican. **Troubleshoot:** * Ensure that Deckhand can authenticate against Keystone. * Ensure that Deckhand's Barbican configuration options are correct. * Ensure that Deckhand and Barbican are contained in the Keystone service catalog. """ msg_fmt = 'Barbican raised a client error. Details: %(details)s' code = 400 # Needs to be overridden. class BarbicanServerException(DeckhandException): """A server-side 5xx error occurred with Barbican.""" msg_fmt = ('Barbican raised a server error. Details: %(details)s') code = 500 class InvalidInputException(DeckhandException): """An Invalid Input provided due to which unable to process request.""" msg_fmt = ('Failed to process request due to invalid input: %(input_var)s') code = 400 class DeepDiffException(DeckhandException): """An Exception occurred while deep diffing""" msg_fmt = 'An Exception occurred while deep diffing. Details: %(details)s' code = 500 class UnknownSubstitutionError(DeckhandException): """An unknown error occurred during substitution. **Troubleshoot:** """ code = 500 def __init__(self, *args, **kwargs): super(UnknownSubstitutionError, self).__init__(*args, **kwargs) dest_args = ('schema', 'layer', 'name') msg_format = ('An unknown exception occurred while trying to perform ' 'substitution using source document [%(src_schema)s, ' '%(src_layer)s] %(src_name)s') if all(x in args for x in dest_args): msg_format += (' contained in document [%(schema)s, %(layer)s]' ' %(name)s') msg_format += '. Details: %(detail)s' self.msg_fmt = msg_format