b03a4522cb
Recently added replacement check incorrectly uses metadata.schema and metadata.name to key on the document -- but it should be schema and metadata.name, the combination of which uniquely defines a document. Change-Id: I6cd1679ad41be38cb78d65ce2763e60f7da390d2
505 lines
17 KiB
Python
505 lines
17 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 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
|