
update dockerfile for python deckhand install add deckhand version to chart 1.0 add chart version 0.2.0 update all packages to latest in requirements.txt update zuul jobs for focal and python 3.8 remove zuul job functional-uwsgi-py38 in favor of functional-docker-py38 update tox config typecast to string in re.sub() function add stestr to test-requirements.txt add SQLAlchemy jsonpickle sphinx-rtd-theme stestr to requirements.txt deprecated function: BarbicanException -> BarbicanClientException fix mock import using unittest fix import collections to collections.abc fix for collections modules for older than python 3.10 versions. deprecated function: json -> to_json deprecated function: werkzeug.contrib.profiler -> werkzeug.middleware.profiler deprecated function: falcon.AIP -> falcon.App deprecation warning: switch from resp.body to resp.text rename fixtures to dh_fixtures because there is an imported module fixtures switch from stream.read to bounded_stream.read deprecated function: falcon process_response needed additional parameter deprecated function: falcon default_exception_handler changed parameter order move from MagicMock object to falcon test generated object to fix incompatability with upgraded Falcon module. Adjust gabbi tests to fix incompatability with upgraded DeepDiff module update Makefile to execute ubuntu_focal update HTK (helmtoolkit) unpin barbican to pass integration tests Use helm 3 in chart build. `helm serve` is removed in helm 3 so this moves to using local `file://` dependencies [0] instead. Change-Id: I180416f480edea1b8968d80c993b3e1fcc95c08d
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.text = yaml.safe_dump(error_response)
|
|
|
|
|
|
def default_exception_handler(req, resp, ex, 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
|