Orchestrator API

dispatcher.lib.helpers

Contents

Source code for dispatcher.lib.helpers

# -*- coding:utf-8 -*-

# Copyright 2015 CRS4
# All Rights Reserved.
#
#    Licensed under the GNU General Public License, version 2 (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.gnu.org/licenses/gpl-2.0.html
#
#    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.


"""
    :copyright: |copy| 2015 by CRS4.
    :license: gpl-2, see License for more details.

    Various helpers, custom exceptions, login mechanism and wrappers

"""

import requests
import logging
import BaseHTTPServer  # For HTTP codes.
from werkzeug.exceptions import HTTPException

from flask import request, current_app, _request_ctx_stack, json, jsonify
from simplejson import JSONDecodeError

from flask.ext import security
from flask.ext.security import decorators
from flask.ext.principal import Identity, identity_changed
from flask.ext.login import current_user

from functools import wraps
#from dispatcher.formats import render_message
from decorator import decorator


HTTP_CODES = BaseHTTPServer.BaseHTTPRequestHandler.responses
LOG = logging.getLogger(__name__)


[docs]class MiddlewareException(HTTPException): """ Cancel abortion of the current task and return with the given message and error code. """ def __init__(self, message, format='json', state='success', code=200, url=None): self.message = message LOG.error("MWException(%s %s): %s" % (state, code, message)) #self.response = render_message(request, message, # format, state=state, code=code, url=url) self.response = make_response(status_code=code, status=state, url=url, message=message) def __str__(self): return self.message
[docs] def get_response(self, environ): return self.response
[docs]def not_implemented(): """ Quick function to reply with a Not implemented response """ raise MiddlewareException('Not implemented', state='error', code=501)
[docs]def make_response(status=None, status_code=200, other_headers=None, **kwargs): """ Return a suitable HTML or JSON error message response. """ short_message, long_message = HTTP_CODES.get(status_code, ('', '')) if status is None: if str(status_code).startswith('2'): status = 'ok' else: status = 'error' result = dict( status=status, status_code=status_code) if status == 'error': result['status_short_message'] = short_message result['status_long_message'] = long_message result.update(kwargs) if result.get('url', '') is None: empty_url = result.pop('url', '') response = jsonify(result) response.status_code = status_code if other_headers: response.headers.extend(other_headers) return response # authentication wrapper ( embed some flask-security methods )
def _check_token(): """ Check token from request object, reply with a boolean to validate it. """ header_key = security.core._security.token_authentication_header args_key = security.core._security.token_authentication_key header_token = request.headers.get(header_key, None) token = request.args.get(args_key, header_token) if request.json: token = request.json.get(args_key, token) serializer = security.core._security.remember_token_serializer try: data = serializer.loads(token) except Exception as e: return False user = security.core._security.datastore.find_user(username=data[0]) token = data[1] encrypted_tokens = set([security.utils.md5(t.token) for t in user.tokens if t.active]) if token in encrypted_tokens: app = current_app._get_current_object() _request_ctx_stack.top.user = user identity_changed.send(app, identity=Identity(user.id)) return True
[docs]def validate_json(validate_function, default=None): """Decorator to validate and marshal the incoming JSON. Sets request.json to the value returned from calling validate_function on request.json (or default() if request.json is None). If this raises an exception then the call stack is terminated early with a :func:`MiddlewareException` exception If request.json is None then default() is used. Note that 1) default is a callable which must create the default value (to avoid accidental re-use of mutables) 2) the result is still passed through the validate_function (to ensure the invariant holds) """ @decorator def validate_with_validate_function(f, *args, **kwargs): try: data = request.data or "{}" input_json = json.loads(data) except JSONDecodeError as e: raise MiddlewareException("Malformed JSON stream", state='error', code=400) # input_json = request.json if input_json is None and callable(default): input_json = default() try: # request.json = validate_function(input_json) request.data = validate_function(input_json) except Exception as e: message = "Some key doesn't validate the schema - %s" % (str(e)) raise MiddlewareException(message, state='error', code=400) return f(*args, **kwargs) return validate_with_validate_function
[docs]def auth_required(*auth_methods): """ Adaptation for security.decorators.auth_required """ login_mechanisms = { 'token': lambda: _check_token(), 'basic': lambda: decorators._check_http_auth(), 'session': lambda: current_user.is_authenticated() } def wrapper(fn): @wraps(fn) def decorated_view(*args, **kwargs): """ decorator farm """ mechanisms = [login_mechanisms.get(method) for method in auth_methods] for mechanism in mechanisms: if mechanism and mechanism(): return fn(*args, **kwargs) raise MiddlewareException('Forbidden', state='error', code=403) return decorated_view return wrapper
[docs]class HTTPConnection(object): """ Class to make connections and deal with web services """ def __init__(self, server, port, api_version, headers=None): self.server = server self.port = port self.api_v = api_version self.api_v_url = "/api/" + self.api_v self.server_url = 'http://%s:%s' % (server, port) self.headers = headers or {'content-type': 'application/json'} self.last_status = None self.last_message = None
[docs] def get_page(self, url, method='get', payload=None): """ Make a generic call with a json response + :attr:`url` is the relative url to contact, protocol and servername\ is added on the fly with what has been specified on\ initialization of the object + :attr:`kwargs` is an attribute of optional key/value pairs, should be: * :attr:`method`, the way to contact the server, default is 'get', * :attr:`payload`, is a dictionary structure containing additional\ payload useful to the request """ url = self.server_url + self.api_v_url + url try: r = requests.__dict__[method](url, data=json.dumps(payload), headers=self.headers) self.last_status = r.status_code res = r.json() try: self.last_message = res except: pass except Exception as e: #print str(e) #LOG.error("ORCHE is down") MiddlewareException('the host %s is down' % self.server, state='error', code=503) res = {} return res
[docs]def get_a_caller(app_context): """ With an app_context ( to access the settings data ) this routines will spawn a caller object to make HTTP calls. Settings are CISTERN_* data """ app = app_context server = app.config['CISTERN_HOST'] port = app.config['CISTERN_PORT'] api_v = app.config['CISTERN_API'] caller = HTTPConnection(server, port, api_v) return caller # from http://flask.pocoo.org/snippets/56/ # http code reminder: # http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html # 201 Created # 202 Accepted # 301 Moved Permanently # 400 Bad Request # 401 Unauthorized # 403 Forbidden # 408 Request Timeout # 500 Internal Server Error # 501 Not Implemented # 503 Service Unavailable

Contents