From 9eb6f9c68678d60f2bad7a3b8b252a7db335a2d9 Mon Sep 17 00:00:00 2001 From: Samantha Blanco Date: Fri, 20 Oct 2017 14:43:24 -0400 Subject: [PATCH] Inital API Commit Creates necessary API files and implements health API route. Change-Id: Id545d65949fcc48a05565f39b08180d4aa86006f --- etc/promenade/api-paste.ini | 25 ++++ promenade/config.py | 3 +- promenade/control/__init__.py | 0 promenade/control/api.py | 75 ++++++++++ promenade/control/base.py | 184 ++++++++++++++++++++++++ promenade/control/health_api.py | 29 ++++ promenade/control/middleware.py | 127 ++++++++++++++++ promenade/exceptions.py | 247 +++++++++++++++++++++++++++++++- promenade/policy.py | 90 ++++++++++++ promenade/promenade.py | 31 ++++ promenade/tar_bundler.py | 3 +- promenade/validation.py | 18 ++- requirements-direct.txt | 4 + requirements-frozen.txt | 6 +- 14 files changed, 832 insertions(+), 10 deletions(-) create mode 100644 etc/promenade/api-paste.ini create mode 100644 promenade/control/__init__.py create mode 100644 promenade/control/api.py create mode 100644 promenade/control/base.py create mode 100644 promenade/control/health_api.py create mode 100644 promenade/control/middleware.py create mode 100644 promenade/policy.py create mode 100644 promenade/promenade.py diff --git a/etc/promenade/api-paste.ini b/etc/promenade/api-paste.ini new file mode 100644 index 00000000..0577c18c --- /dev/null +++ b/etc/promenade/api-paste.ini @@ -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 diff --git a/promenade/config.py b/promenade/config.py index 8ca8c0a6..bec4deda 100644 --- a/promenade/config.py +++ b/promenade/config.py @@ -147,7 +147,8 @@ def _matches_filter(document, *, schema, labels): def _get(documents, kind=None, schema=None, name=None): if kind 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 for document in documents: diff --git a/promenade/control/__init__.py b/promenade/control/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/promenade/control/api.py b/promenade/control/api.py new file mode 100644 index 00000000..5824618c --- /dev/null +++ b/promenade/control/api.py @@ -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 diff --git a/promenade/control/base.py b/promenade/control/base.py new file mode 100644 index 00000000..11571596 --- /dev/null +++ b/promenade/control/base.py @@ -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 diff --git a/promenade/control/health_api.py b/promenade/control/health_api.py new file mode 100644 index 00000000..011e45f2 --- /dev/null +++ b/promenade/control/health_api.py @@ -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 diff --git a/promenade/control/middleware.py b/promenade/control/middleware.py new file mode 100644 index 00000000..9c0ecd10 --- /dev/null +++ b/promenade/control/middleware.py @@ -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) diff --git a/promenade/exceptions.py b/promenade/exceptions.py index edfd7827..34472eb0 100644 --- a/promenade/exceptions.py +++ b/promenade/exceptions.py @@ -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 traceback + +import falcon 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): + """ + Base error containing enough information to make a promenade-formatted + error + """ EXIT_CODE = 1 - def __init__(self, message, *, trace=True): - self.message = message + def __init__(self, + 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 + 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): if self.trace or debug: - LOG.exception(self.message) + LOG.exception(self.description) 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): - 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 diff --git a/promenade/policy.py b/promenade/policy.py new file mode 100644 index 00000000..4752081d --- /dev/null +++ b/promenade/policy.py @@ -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 diff --git a/promenade/promenade.py b/promenade/promenade.py new file mode 100644 index 00000000..49b4ab96 --- /dev/null +++ b/promenade/promenade.py @@ -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() diff --git a/promenade/tar_bundler.py b/promenade/tar_bundler.py index a0d96953..c697c4f9 100644 --- a/promenade/tar_bundler.py +++ b/promenade/tar_bundler.py @@ -1,8 +1,9 @@ -from . import logging import hashlib import io import tarfile +from promenade import logging + __all__ = ['TarBundler'] LOG = logging.getLogger(__name__) diff --git a/promenade/validation.py b/promenade/validation.py index 0b99cdda..8d69a664 100644 --- a/promenade/validation.py +++ b/promenade/validation.py @@ -1,5 +1,19 @@ -from . import exceptions -from . import logging +# 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 import exceptions +from promenade import logging import jsonschema import os import pkg_resources diff --git a/requirements-direct.txt b/requirements-direct.txt index 6f03da8d..9fb7fe6c 100644 --- a/requirements-direct.txt +++ b/requirements-direct.txt @@ -1,7 +1,11 @@ click==6.7 +falcon==1.2.0 jinja2==2.9.6 jsonpath-ng==1.4.3 jsonschema==2.6.0 +keystonemiddleware==4.17.0 +oslo.context>=2.14.0 +PasteDeploy==1.5.2 pbr==3.0.1 pyyaml==3.12 requests==2.18.4 diff --git a/requirements-frozen.txt b/requirements-frozen.txt index 54d3d641..4b0a6174 100644 --- a/requirements-frozen.txt +++ b/requirements-frozen.txt @@ -2,14 +2,18 @@ certifi==2017.7.27.1 chardet==3.0.4 click==6.7 decorator==4.1.2 +falcon==1.2.0 idna==2.6 Jinja2==2.9.6 jsonpath-ng==1.4.3 jsonschema==2.6.0 +keystonemiddleware==4.17.0 MarkupSafe==1.0 +oslo.context==2.19.1 +PasteDeploy==1.5.2 pbr==3.0.1 ply==3.10 PyYAML==3.12 requests==2.18.4 six==1.11.0 -urllib3==1.22 +urllib3==1.22 \ No newline at end of file