Inital API Commit
Creates necessary API files and implements health API route. Change-Id: Id545d65949fcc48a05565f39b08180d4aa86006f
This commit is contained in:
parent
4dd7e64f0b
commit
9eb6f9c686
|
@ -0,0 +1,25 @@
|
||||||
|
# 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.
|
||||||
|
|
||||||
|
#PasteDeploy Configuration File
|
||||||
|
#Used to configure uWSGI middleware pipeline
|
||||||
|
|
||||||
|
[filter:authtoken]
|
||||||
|
paste.filter_factory = keystonemiddleware.auth_token:filter_factory
|
||||||
|
|
||||||
|
[app:promenade-api]
|
||||||
|
paste.app_factory = promenade.promenade:paste_start_promenade
|
||||||
|
|
||||||
|
[pipeline:main]
|
||||||
|
pipeline = authtoken promenade-api
|
|
@ -147,7 +147,8 @@ def _matches_filter(document, *, schema, labels):
|
||||||
def _get(documents, kind=None, schema=None, name=None):
|
def _get(documents, kind=None, schema=None, name=None):
|
||||||
if kind is not None:
|
if kind is not None:
|
||||||
if schema is not None:
|
if schema is not None:
|
||||||
raise AssertionError('Logic error: specified both kind and schema')
|
msg = "Only kind or schema may be specified, not both"
|
||||||
|
raise exceptions.ValidationException(msg)
|
||||||
schema = 'promenade/%s/v1' % kind
|
schema = 'promenade/%s/v1' % kind
|
||||||
|
|
||||||
for document in documents:
|
for document in documents:
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
# 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 promenade.control.base import BaseResource, PromenadeRequest
|
||||||
|
from promenade.control.health_api import HealthResource
|
||||||
|
from promenade.control.middleware import (AuthMiddleware, ContextMiddleware,
|
||||||
|
LoggingMiddleware)
|
||||||
|
from promenade import exceptions as exc
|
||||||
|
from promenade import logging
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def start_api():
|
||||||
|
middlewares = [
|
||||||
|
AuthMiddleware(),
|
||||||
|
ContextMiddleware(),
|
||||||
|
LoggingMiddleware(),
|
||||||
|
]
|
||||||
|
control_api = falcon.API(
|
||||||
|
request_type=PromenadeRequest, middleware=middlewares)
|
||||||
|
|
||||||
|
# v1.0 of Promenade API
|
||||||
|
v1_0_routes = [
|
||||||
|
# API for managing region data
|
||||||
|
('/health', HealthResource()),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Set up the 1.0 routes
|
||||||
|
route_v1_0_prefix = '/api/v1.0'
|
||||||
|
for path, res in v1_0_routes:
|
||||||
|
route = '{}{}'.format(route_v1_0_prefix, path)
|
||||||
|
LOG.info('Adding route: %s Handled by %s', route,
|
||||||
|
res.__class__.__name__)
|
||||||
|
control_api.add_route(route, res)
|
||||||
|
|
||||||
|
control_api.add_route('/versions', VersionsResource())
|
||||||
|
|
||||||
|
# Error handlers (FILO handling)
|
||||||
|
control_api.add_error_handler(Exception, exc.default_exception_handler)
|
||||||
|
control_api.add_error_handler(exc.PromenadeException,
|
||||||
|
exc.PromenadeException.handle)
|
||||||
|
|
||||||
|
# built-in error serializer
|
||||||
|
control_api.set_error_serializer(exc.default_error_serializer)
|
||||||
|
|
||||||
|
return control_api
|
||||||
|
|
||||||
|
|
||||||
|
class VersionsResource(BaseResource):
|
||||||
|
"""
|
||||||
|
Lists the versions supported by this API
|
||||||
|
"""
|
||||||
|
|
||||||
|
def on_get(self, req, resp):
|
||||||
|
resp.body = self.to_json({
|
||||||
|
'v1.0': {
|
||||||
|
'path': '/api/v1.0',
|
||||||
|
'status': 'stable'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
resp.status = falcon.HTTP_200
|
|
@ -0,0 +1,184 @@
|
||||||
|
# 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 uuid
|
||||||
|
|
||||||
|
from oslo_context import context
|
||||||
|
from jsonschema import validate
|
||||||
|
|
||||||
|
import falcon
|
||||||
|
import falcon.request as request
|
||||||
|
import falcon.routing as routing
|
||||||
|
|
||||||
|
from promenade import exceptions as exc
|
||||||
|
from promenade import logging
|
||||||
|
|
||||||
|
|
||||||
|
class BaseResource(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.logger = logging.getLogger('promenade.control')
|
||||||
|
|
||||||
|
def on_options(self, req, resp, **kwargs):
|
||||||
|
"""
|
||||||
|
Handle options requests
|
||||||
|
"""
|
||||||
|
method_map = routing.create_http_method_map(self)
|
||||||
|
for method in method_map:
|
||||||
|
if method_map.get(method).__name__ != 'method_not_allowed':
|
||||||
|
resp.append_header('Allow', method)
|
||||||
|
resp.status = falcon.HTTP_200
|
||||||
|
|
||||||
|
def req_json(self, req, validate_json_schema=None):
|
||||||
|
"""
|
||||||
|
Reads and returns the input json message, optionally validates against
|
||||||
|
a provided jsonschema
|
||||||
|
:param req: the falcon request object
|
||||||
|
:param validate_json_schema: the optional jsonschema to use for
|
||||||
|
validation
|
||||||
|
"""
|
||||||
|
has_input = False
|
||||||
|
if ((req.content_length is not None or req.content_length != 0)
|
||||||
|
and (req.content_type is not None
|
||||||
|
and req.content_type.lower() == 'application/json')):
|
||||||
|
raw_body = req.stream.read(req.content_length or 0)
|
||||||
|
if raw_body is not None:
|
||||||
|
has_input = True
|
||||||
|
self.info(req.context, 'Input message body: %s' % raw_body)
|
||||||
|
else:
|
||||||
|
self.info(req.context, 'No message body specified')
|
||||||
|
if has_input:
|
||||||
|
# read the json and validate if necessary
|
||||||
|
try:
|
||||||
|
raw_body = raw_body.decode('utf-8')
|
||||||
|
json_body = json.loads(raw_body)
|
||||||
|
if validate_json_schema:
|
||||||
|
# raises an exception if it doesn't validate
|
||||||
|
validate(json_body, json.loads(validate_json_schema))
|
||||||
|
return json_body
|
||||||
|
except json.JSONDecodeError as jex:
|
||||||
|
self.error(req.context,
|
||||||
|
"Invalid JSON in request: \n%s" % raw_body)
|
||||||
|
raise exc.InvalidFormatError(
|
||||||
|
title='JSON could not be decoded',
|
||||||
|
description='%s: Invalid JSON in body: %s' % (req.path,
|
||||||
|
jex))
|
||||||
|
else:
|
||||||
|
# No body passed as input. Fail validation if it was asekd for
|
||||||
|
if validate_json_schema is not None:
|
||||||
|
raise exc.InvalidFormatError(
|
||||||
|
title='Json body is required',
|
||||||
|
description='%s: Bad input, no body provided' % (req.path))
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def to_json(self, body_dict):
|
||||||
|
"""
|
||||||
|
Thin wrapper around json.dumps, providing the default=str config
|
||||||
|
"""
|
||||||
|
return json.dumps(body_dict, default=str)
|
||||||
|
|
||||||
|
def log_message(self, ctx, level, msg):
|
||||||
|
"""
|
||||||
|
Logs a message with context, and extra populated.
|
||||||
|
"""
|
||||||
|
extra = {'user': 'N/A', 'req_id': 'N/A', 'external_ctx': 'N/A'}
|
||||||
|
|
||||||
|
if ctx is not None:
|
||||||
|
extra = {
|
||||||
|
'user': ctx.user,
|
||||||
|
'req_id': ctx.request_id,
|
||||||
|
'external_ctx': ctx.external_marker,
|
||||||
|
}
|
||||||
|
|
||||||
|
self.logger.log(level, msg, extra=extra)
|
||||||
|
|
||||||
|
def debug(self, ctx, msg):
|
||||||
|
"""
|
||||||
|
Debug logger for resources, incorporating context.
|
||||||
|
"""
|
||||||
|
self.log_message(ctx, logging.DEBUG, msg)
|
||||||
|
|
||||||
|
def info(self, ctx, msg):
|
||||||
|
"""
|
||||||
|
Info logger for resources, incorporating context.
|
||||||
|
"""
|
||||||
|
self.log_message(ctx, logging.INFO, msg)
|
||||||
|
|
||||||
|
def warn(self, ctx, msg):
|
||||||
|
"""
|
||||||
|
Warn logger for resources, incorporating context.
|
||||||
|
"""
|
||||||
|
self.log_message(ctx, logging.WARN, msg)
|
||||||
|
|
||||||
|
def error(self, ctx, msg):
|
||||||
|
"""
|
||||||
|
Error logger for resources, incorporating context.
|
||||||
|
"""
|
||||||
|
self.log_message(ctx, logging.ERROR, msg)
|
||||||
|
|
||||||
|
|
||||||
|
class PromenadeRequestContext(context.RequestContext):
|
||||||
|
"""
|
||||||
|
Context object for promenade resource requests
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, external_marker=None, policy_engine=None, **kwargs):
|
||||||
|
self.log_level = 'error'
|
||||||
|
self.request_id = str(uuid.uuid4())
|
||||||
|
self.external_marker = external_marker
|
||||||
|
self.policy_engine = policy_engine
|
||||||
|
self.is_admin_project = False
|
||||||
|
self.authenticated = False
|
||||||
|
super(PromenadeRequestContext, self).__init__(**kwargs)
|
||||||
|
|
||||||
|
def set_log_level(self, level):
|
||||||
|
if level in ['error', 'info', 'debug']:
|
||||||
|
self.log_level = level
|
||||||
|
|
||||||
|
def set_user(self, user):
|
||||||
|
self.user = user
|
||||||
|
|
||||||
|
def set_project(self, project):
|
||||||
|
self.project = project
|
||||||
|
|
||||||
|
def add_role(self, role):
|
||||||
|
self.roles.append(role)
|
||||||
|
|
||||||
|
def add_roles(self, roles):
|
||||||
|
self.roles.extend(roles)
|
||||||
|
|
||||||
|
def remove_role(self, role):
|
||||||
|
self.roles = [x for x in self.roles if x != role]
|
||||||
|
|
||||||
|
def set_external_marker(self, marker):
|
||||||
|
self.external_marker = marker
|
||||||
|
|
||||||
|
def set_policy_engine(self, engine):
|
||||||
|
self.policy_engine = engine
|
||||||
|
|
||||||
|
def to_policy_view(self):
|
||||||
|
policy_dict = {}
|
||||||
|
|
||||||
|
policy_dict['user_id'] = self.user_id
|
||||||
|
policy_dict['user_domain_id'] = self.user_domain_id
|
||||||
|
policy_dict['project_id'] = self.project_id
|
||||||
|
policy_dict['project_domain_id'] = self.project_domain_id
|
||||||
|
policy_dict['roles'] = self.roles
|
||||||
|
policy_dict['is_admin_project'] = self.is_admin_project
|
||||||
|
|
||||||
|
return policy_dict
|
||||||
|
|
||||||
|
|
||||||
|
class PromenadeRequest(request.Request):
|
||||||
|
context_type = PromenadeRequestContext
|
|
@ -0,0 +1,29 @@
|
||||||
|
# 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 promenade.control import base
|
||||||
|
|
||||||
|
|
||||||
|
class HealthResource(base.BaseResource):
|
||||||
|
"""
|
||||||
|
Return empty response/body to show
|
||||||
|
that promenade is healthy
|
||||||
|
"""
|
||||||
|
|
||||||
|
def on_get(self, req, resp):
|
||||||
|
"""
|
||||||
|
It really does nothing right now. It may do more later
|
||||||
|
"""
|
||||||
|
resp.status = falcon.HTTP_204
|
|
@ -0,0 +1,127 @@
|
||||||
|
# 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 uuid
|
||||||
|
|
||||||
|
from promenade import logging
|
||||||
|
from promenade import policy
|
||||||
|
|
||||||
|
|
||||||
|
class AuthMiddleware(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.logger = logging.getLogger('promenade')
|
||||||
|
|
||||||
|
# Authentication
|
||||||
|
def process_request(self, req, resp):
|
||||||
|
ctx = req.context
|
||||||
|
ctx.set_policy_engine(policy.policy_engine)
|
||||||
|
|
||||||
|
for k, v in req.headers.items():
|
||||||
|
self.logger.debug("Request with header %s: %s" % (k, v))
|
||||||
|
|
||||||
|
auth_status = req.get_header(
|
||||||
|
'X-SERVICE-IDENTITY-STATUS') # will be set to Confirmed or Invalid
|
||||||
|
service = True
|
||||||
|
|
||||||
|
if auth_status is None:
|
||||||
|
auth_status = req.get_header('X-IDENTITY-STATUS')
|
||||||
|
service = False
|
||||||
|
|
||||||
|
if auth_status == 'Confirmed':
|
||||||
|
# Process account and roles
|
||||||
|
ctx.authenticated = True
|
||||||
|
# User Identity, unique within owning domain
|
||||||
|
ctx.user = req.get_header(
|
||||||
|
'X-SERVICE-USER-NAME') if service else req.get_header(
|
||||||
|
'X-USER-NAME')
|
||||||
|
# Identity-service managed unique identifier
|
||||||
|
ctx.user_id = req.get_header(
|
||||||
|
'X-SERVICE-USER-ID') if service else req.get_header(
|
||||||
|
'X-USER-ID')
|
||||||
|
# Identity service managed unique identifier of owning domain of
|
||||||
|
# user name
|
||||||
|
ctx.user_domain_id = req.get_header(
|
||||||
|
'X-SERVICE-USER-DOMAIN-ID') if service else req.get_header(
|
||||||
|
'X-USER-DOMAIN-ID')
|
||||||
|
# Identity service managed unique identifier
|
||||||
|
ctx.project_id = req.get_header(
|
||||||
|
'X-SERVICE-PROJECT-ID') if service else req.get_header(
|
||||||
|
'X-PROJECT-ID')
|
||||||
|
# Name of owning domain of project
|
||||||
|
ctx.project_domain_id = req.get_header(
|
||||||
|
'X-SERVICE-PROJECT-DOMAIN-ID') if service else req.get_header(
|
||||||
|
'X-PROJECT-DOMAIN-NAME')
|
||||||
|
if service:
|
||||||
|
# comma delimieted list of case-sensitive role names
|
||||||
|
ctx.add_roles(req.get_header('X-SERVICE-ROLES').split(','))
|
||||||
|
else:
|
||||||
|
ctx.add_roles(req.get_header('X-ROLES').split(','))
|
||||||
|
|
||||||
|
if req.get_header('X-IS-ADMIN-PROJECT') == 'True':
|
||||||
|
ctx.is_admin_project = True
|
||||||
|
else:
|
||||||
|
ctx.is_admin_project = False
|
||||||
|
|
||||||
|
self.logger.debug(
|
||||||
|
'Request from authenticated user %s with roles %s', ctx.user,
|
||||||
|
','.join(ctx.roles))
|
||||||
|
else:
|
||||||
|
ctx.authenticated = False
|
||||||
|
|
||||||
|
|
||||||
|
class ContextMiddleware(object):
|
||||||
|
"""
|
||||||
|
Handle looking at the X-Context_Marker to see if it has value and that
|
||||||
|
value is a UUID (or close enough). If not, generate one.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _format_uuid_string(self, string):
|
||||||
|
return (string.replace('urn:', '').replace('uuid:', '').strip('{}')
|
||||||
|
.replace('-', '').lower())
|
||||||
|
|
||||||
|
def _is_uuid_like(self, val):
|
||||||
|
try:
|
||||||
|
return str(uuid.UUID(val)).replace(
|
||||||
|
'-', '') == self._format_uuid_string(val)
|
||||||
|
except (TypeError, ValueError, AttributeError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def process_request(self, req, resp):
|
||||||
|
ctx = req.context
|
||||||
|
ext_marker = req.get_header('X-Context-Marker')
|
||||||
|
if ext_marker is not None and self.is_uuid_like(ext_marker):
|
||||||
|
# external passed in an ok context marker
|
||||||
|
ctx.set_external_marker(ext_marker)
|
||||||
|
else:
|
||||||
|
# use the request id
|
||||||
|
ctx.set_external_marker(ctx.request_id)
|
||||||
|
|
||||||
|
|
||||||
|
class LoggingMiddleware(object):
|
||||||
|
def __init__(self):
|
||||||
|
self.logger = logging.getLogger('promenade.control')
|
||||||
|
|
||||||
|
def process_response(self, req, resp, resource, req_succeeded):
|
||||||
|
ctx = req.context
|
||||||
|
|
||||||
|
extra = {
|
||||||
|
'user': ctx.user,
|
||||||
|
'req_id': ctx.request_id,
|
||||||
|
'external_ctx': ctx.external_marker,
|
||||||
|
}
|
||||||
|
|
||||||
|
resp.append_header('X-Promenade-Req', ctx.request_id)
|
||||||
|
self.logger.info(
|
||||||
|
'%s %s - %s', req.method, req.uri, resp.status, extra=extra)
|
||||||
|
self.logger.debug('Response body:\n%s', resp.body, extra=extra)
|
|
@ -1,21 +1,258 @@
|
||||||
|
# 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 logging
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
import falcon
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
# Standard error handler
|
||||||
|
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.
|
||||||
|
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: 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': 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
|
||||||
|
|
||||||
|
version = 'N/A'
|
||||||
|
|
||||||
|
for part in req.path.split('/'):
|
||||||
|
if '.' in part and part.startswith('v'):
|
||||||
|
version = part
|
||||||
|
break
|
||||||
|
|
||||||
|
error_response = {
|
||||||
|
'kind': 'status',
|
||||||
|
'apiVersion': version,
|
||||||
|
'metadata': {},
|
||||||
|
'status': 'Failure',
|
||||||
|
'message': message,
|
||||||
|
'reason': reason,
|
||||||
|
'details': {
|
||||||
|
'errorType': error_type,
|
||||||
|
'errorCount': len(error_list),
|
||||||
|
'messageList': message_list
|
||||||
|
},
|
||||||
|
'code': status_code,
|
||||||
|
'retry': retry
|
||||||
|
}
|
||||||
|
|
||||||
|
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_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': True
|
||||||
|
}],
|
||||||
|
info_list=None)
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
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()
|
||||||
|
LOG.error('Unhanded Exception being handled: \n%s', exc_string)
|
||||||
|
format_error_resp(
|
||||||
|
req,
|
||||||
|
resp,
|
||||||
|
falcon.HTTP_500,
|
||||||
|
error_type=ex.__class__.__name__,
|
||||||
|
message="Unhandled Exception raised: %s" % str(ex),
|
||||||
|
retry=True)
|
||||||
|
|
||||||
|
|
||||||
class PromenadeException(Exception):
|
class PromenadeException(Exception):
|
||||||
|
"""
|
||||||
|
Base error containing enough information to make a promenade-formatted
|
||||||
|
error
|
||||||
|
"""
|
||||||
EXIT_CODE = 1
|
EXIT_CODE = 1
|
||||||
|
|
||||||
def __init__(self, message, *, trace=True):
|
def __init__(self,
|
||||||
self.message = message
|
title=None,
|
||||||
|
description=None,
|
||||||
|
error_list=None,
|
||||||
|
info_list=None,
|
||||||
|
status=None,
|
||||||
|
retry=False,
|
||||||
|
trace=False):
|
||||||
|
"""
|
||||||
|
:param description: The internal error description
|
||||||
|
:param error_list: The list of errors
|
||||||
|
:param status: The desired falcon HTTP response 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
|
||||||
|
:param trace: Return traceback
|
||||||
|
"""
|
||||||
|
|
||||||
|
self.title = title or self.__class__.title
|
||||||
|
|
||||||
|
self.status = status or self.__class__.status
|
||||||
|
|
||||||
|
self.description = description
|
||||||
|
self.error_list = massage_error_list(error_list, description)
|
||||||
|
self.info_list = info_list
|
||||||
|
self.retry = retry
|
||||||
self.trace = trace
|
self.trace = trace
|
||||||
|
super().__init__(
|
||||||
|
PromenadeException._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):
|
||||||
|
"""
|
||||||
|
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)
|
||||||
|
|
||||||
def display(self, debug=False):
|
def display(self, debug=False):
|
||||||
if self.trace or debug:
|
if self.trace or debug:
|
||||||
LOG.exception(self.message)
|
LOG.exception(self.description)
|
||||||
else:
|
else:
|
||||||
LOG.error(self.message)
|
LOG.error(self.description)
|
||||||
|
|
||||||
|
|
||||||
|
class ApiError(PromenadeException):
|
||||||
|
"""
|
||||||
|
An error to handle general api errors.
|
||||||
|
"""
|
||||||
|
|
||||||
|
title = 'Api Error'
|
||||||
|
status = falcon.HTTP_400
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidFormatError(PromenadeException):
|
||||||
|
"""
|
||||||
|
An exception to cover invalid input formatting
|
||||||
|
"""
|
||||||
|
|
||||||
|
title = 'Invalid Input Error'
|
||||||
|
status = falcon.HTTP_400
|
||||||
|
|
||||||
|
|
||||||
class ValidationException(PromenadeException):
|
class ValidationException(PromenadeException):
|
||||||
pass
|
title = 'Validation Error'
|
||||||
|
|
||||||
|
|
||||||
|
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.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})
|
||||||
|
return output_error_list
|
||||||
|
|
|
@ -0,0 +1,90 @@
|
||||||
|
# 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 functools
|
||||||
|
|
||||||
|
import falcon
|
||||||
|
|
||||||
|
from promenade import exceptions as ex
|
||||||
|
|
||||||
|
# TODO: Add policy_engine
|
||||||
|
policy_engine = None
|
||||||
|
|
||||||
|
|
||||||
|
class ApiEnforcer(object):
|
||||||
|
"""
|
||||||
|
A decorator class for enforcing RBAC policies
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, action):
|
||||||
|
self.action = action
|
||||||
|
|
||||||
|
def __call__(self, f):
|
||||||
|
@functools.wraps(f)
|
||||||
|
def secure_handler(slf, req, resp, *args, **kwargs):
|
||||||
|
ctx = req.context
|
||||||
|
policy_eng = ctx.policy_engine
|
||||||
|
slf.info(ctx, "Policy Engine: %s" % policy_eng.__class__.__name__)
|
||||||
|
# perform auth
|
||||||
|
slf.info(ctx, "Enforcing policy %s on request %s" %
|
||||||
|
(self.action, ctx.request_id))
|
||||||
|
# policy engine must be configured
|
||||||
|
if policy_eng is None:
|
||||||
|
slf.error(
|
||||||
|
ctx,
|
||||||
|
"Error-Policy engine required-action: %s" % self.action)
|
||||||
|
raise ex.PromenadeException(
|
||||||
|
title="Auth is not being handled by any policy engine",
|
||||||
|
status=falcon.HTTP_500,
|
||||||
|
retry=False)
|
||||||
|
authorized = False
|
||||||
|
try:
|
||||||
|
if policy_eng.authorize(self.action, ctx):
|
||||||
|
# authorized
|
||||||
|
slf.info(ctx, "Request is authorized")
|
||||||
|
authorized = True
|
||||||
|
except Exception:
|
||||||
|
# couldn't service the auth request
|
||||||
|
slf.error(
|
||||||
|
ctx,
|
||||||
|
"Error - Expectation Failed - action: %s" % self.action)
|
||||||
|
raise ex.ApiError(
|
||||||
|
title="Expectation Failed",
|
||||||
|
status=falcon.HTTP_417,
|
||||||
|
retry=False)
|
||||||
|
if authorized:
|
||||||
|
return f(slf, req, resp, *args, **kwargs)
|
||||||
|
else:
|
||||||
|
slf.error(
|
||||||
|
ctx,
|
||||||
|
"Auth check failed. Authenticated:%s" % ctx.authenticated)
|
||||||
|
# raise the appropriate response exeception
|
||||||
|
if ctx.authenticated:
|
||||||
|
slf.error(
|
||||||
|
ctx,
|
||||||
|
"Error: Forbidden access - action: %s" % self.action)
|
||||||
|
raise ex.ApiError(
|
||||||
|
title="Forbidden",
|
||||||
|
status=falcon.HTTP_403,
|
||||||
|
description="Credentials do not permit access",
|
||||||
|
retry=False)
|
||||||
|
else:
|
||||||
|
slf.error(ctx, "Error - Unauthenticated access")
|
||||||
|
raise ex.ApiError(
|
||||||
|
title="Unauthenticated",
|
||||||
|
status=falcon.HTTP_401,
|
||||||
|
description="Credentials are not established",
|
||||||
|
retry=False)
|
||||||
|
|
||||||
|
return secure_handler
|
|
@ -0,0 +1,31 @@
|
||||||
|
# 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 promenade.control import api
|
||||||
|
from promenade import logging
|
||||||
|
|
||||||
|
|
||||||
|
def start_promenade():
|
||||||
|
# Setup root logger
|
||||||
|
logging.setup(verbose=False)
|
||||||
|
# TODO: Add policy engine to start
|
||||||
|
# Start the API
|
||||||
|
return api.start_api()
|
||||||
|
|
||||||
|
|
||||||
|
# Initialization compatible with PasteDeploy
|
||||||
|
def paste_start_promenade(global_conf, **kwargs):
|
||||||
|
return promenade
|
||||||
|
|
||||||
|
|
||||||
|
promenade = start_promenade()
|
|
@ -1,8 +1,9 @@
|
||||||
from . import logging
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import io
|
import io
|
||||||
import tarfile
|
import tarfile
|
||||||
|
|
||||||
|
from promenade import logging
|
||||||
|
|
||||||
__all__ = ['TarBundler']
|
__all__ = ['TarBundler']
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
|
@ -1,5 +1,19 @@
|
||||||
from . import exceptions
|
# Copyright 2017 AT&T Intellectual Property. All other rights reserved.
|
||||||
from . import logging
|
#
|
||||||
|
# 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 promenade import exceptions
|
||||||
|
from promenade import logging
|
||||||
import jsonschema
|
import jsonschema
|
||||||
import os
|
import os
|
||||||
import pkg_resources
|
import pkg_resources
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
click==6.7
|
click==6.7
|
||||||
|
falcon==1.2.0
|
||||||
jinja2==2.9.6
|
jinja2==2.9.6
|
||||||
jsonpath-ng==1.4.3
|
jsonpath-ng==1.4.3
|
||||||
jsonschema==2.6.0
|
jsonschema==2.6.0
|
||||||
|
keystonemiddleware==4.17.0
|
||||||
|
oslo.context>=2.14.0
|
||||||
|
PasteDeploy==1.5.2
|
||||||
pbr==3.0.1
|
pbr==3.0.1
|
||||||
pyyaml==3.12
|
pyyaml==3.12
|
||||||
requests==2.18.4
|
requests==2.18.4
|
||||||
|
|
|
@ -2,14 +2,18 @@ certifi==2017.7.27.1
|
||||||
chardet==3.0.4
|
chardet==3.0.4
|
||||||
click==6.7
|
click==6.7
|
||||||
decorator==4.1.2
|
decorator==4.1.2
|
||||||
|
falcon==1.2.0
|
||||||
idna==2.6
|
idna==2.6
|
||||||
Jinja2==2.9.6
|
Jinja2==2.9.6
|
||||||
jsonpath-ng==1.4.3
|
jsonpath-ng==1.4.3
|
||||||
jsonschema==2.6.0
|
jsonschema==2.6.0
|
||||||
|
keystonemiddleware==4.17.0
|
||||||
MarkupSafe==1.0
|
MarkupSafe==1.0
|
||||||
|
oslo.context==2.19.1
|
||||||
|
PasteDeploy==1.5.2
|
||||||
pbr==3.0.1
|
pbr==3.0.1
|
||||||
ply==3.10
|
ply==3.10
|
||||||
PyYAML==3.12
|
PyYAML==3.12
|
||||||
requests==2.18.4
|
requests==2.18.4
|
||||||
six==1.11.0
|
six==1.11.0
|
||||||
urllib3==1.22
|
urllib3==1.22
|
Loading…
Reference in New Issue