######### # Copyright (c) 2013-2019 Cloudify Platform Ltd. All 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 traceback import os import yaml from contextlib import contextmanager from flask_restful import Api from flask import Flask, jsonify, Blueprint, current_app from flask_security import Security from sqlalchemy.exc import OperationalError from werkzeug.exceptions import InternalServerError from cloudify._compat import StringIO from manager_rest import config, premium_enabled, manager_exceptions from manager_rest.storage import db, user_datastore from manager_rest.security.user_handler import user_loader from manager_rest.maintenance import maintenance_mode_handler from manager_rest.rest.endpoint_mapper import setup_resources from manager_rest.flask_utils import set_flask_security_config from manager_rest.manager_exceptions import INTERNAL_SERVER_ERROR_CODE from manager_rest.app_logging import (setup_logger, log_request, log_response) if premium_enabled: from cloudify_premium.authentication.extended_auth_handler \ import configure_auth from cloudify_premium.license.license import LicenseHandler SQL_DIALECT = 'postgresql' app_errors = Blueprint('app_errors', __name__) @app_errors.app_errorhandler(manager_exceptions.ManagerException) def manager_exception(error): current_app.logger.error(error) return jsonify( message=str(error), error_code=error.error_code, # useless, but v1 and v2 api clients require server_traceback # remove this after dropping v1 and v2 api clients server_traceback=None ), error.status_code @app_errors.app_errorhandler(InternalServerError) def internal_error(e): s_traceback = StringIO() traceback.print_exc(file=s_traceback) return jsonify( message="Internal error occurred in manager REST server - {0}: {1}" .format(type(e).__name__, e), error_code=INTERNAL_SERVER_ERROR_CODE, server_traceback=s_traceback.getvalue() ), 500 def cope_with_db_failover(): try: db.engine.execute('SELECT 1') except OperationalError as err: current_app.logger.warning( 'Database reconnection occurred. This is expected to happen when ' 'there has been a recent failover or DB proxy restart. ' 'Error was: {err}'.format(err=err) ) class CloudifyFlaskApp(Flask): def __init__(self, load_config=True): _detect_debug_environment() super(CloudifyFlaskApp, self).__init__(__name__) if load_config: config.instance.load_configuration() self._set_sql_alchemy() # This must be the first before_request, otherwise db access may break # after db failovers or db proxy restarts self.before_request(cope_with_db_failover) # These two need to be called after the configuration was loaded if config.instance.rest_service_log_path: setup_logger(self.logger) if premium_enabled and config.instance.file_server_root: self.external_auth = configure_auth(self.logger) self.before_request(LicenseHandler.check_license_expiration_date) else: self.external_auth = None self.before_request(log_request) self.before_request(maintenance_mode_handler) self.after_request(log_response) self._set_flask_security() with self.app_context(): roles = config.instance.authorization_roles if roles: for role in roles: user_datastore.find_or_create_role(name=role['name']) user_datastore.commit() with self._prevent_flask_restful_error_handling(): setup_resources(Api(self)) self.register_blueprint(app_errors) def _set_flask_security(self): """Set Flask-Security specific configurations and init the extension """ set_flask_security_config(self) Security(app=self, datastore=user_datastore) # Get the login manager and set our own callback to be the user getter login_manager = self.extensions['security'].login_manager login_manager.request_loader(user_loader) self.token_serializer = self.extensions[ 'security'].remember_token_serializer def _set_sql_alchemy(self): """ Set SQLAlchemy specific configurations, init the db object and create the tables if necessary """ self.config['SQLALCHEMY_POOL_SIZE'] = 1 self.config['SQLALCHEMY_DATABASE_URI'] = config.instance.db_url self.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False db.init_app(self) # Prepare the app for use with flask-sqlalchemy @contextmanager def _prevent_flask_restful_error_handling(self): """Add flask-restful under this, to avoid installing its errorhandlers Flask-restful's errorhandlers are both not flexible enough, and too complex. We want to simply use flask's error handling mechanism, so this will make sure that flask-restful's are overridden with the default ones. """ orig_handle_exc = self.handle_exception orig_handle_user_exc = self.handle_user_exception yield self.handle_exception = orig_handle_exc self.handle_user_exception = orig_handle_user_exc def reset_app(configuration=None): global app config.reset(configuration) app = CloudifyFlaskApp(False) def _detect_debug_environment(): """ Detect whether server is running in a debug environment if so, connect to debug server at a port stored in env[DEBUG_REST_SERVICE] """ try: docl_debug_path = os.environ.get('DEBUG_CONFIG') if docl_debug_path and os.path.isfile(docl_debug_path): with open(docl_debug_path, 'r') as docl_debug_file: debug_config = yaml.safe_load(docl_debug_file) if debug_config.get('is_debug_on'): import pydevd pydevd.settrace( debug_config['host'], port=53100, stdoutToServer=True, stderrToServer=True, suspend=False) except BaseException as e: raise Exception('Failed to connect to debug server, {0}: {1}'. format(type(e).__name__, str(e)))