Commit 2b26c04a authored by Daniele Venzano's avatar Daniele Venzano

Convert the rest API to pure tornado, without flask

parent 3c132ad4
......@@ -16,11 +16,11 @@
"""Zoe API entrypoint module."""
import logging
import os
from flask import Flask
from tornado.wsgi import WSGIContainer
from tornado.httpserver import HTTPServer
from tornado.ioloop import IOLoop, PeriodicCallback
from tornado.web import Application
import zoe_lib.config as config
import zoe_api.db_init
......@@ -49,26 +49,26 @@ def zoe_web_main() -> int:
log.error("LDAP authentication requested, but 'pyldap' module not installed.")
return 1
log.info("Starting HTTP server...")
app = Flask(__name__, static_url_path='/does-not-exist')
app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024
zoe_api.db_init.init()
api_endpoint = zoe_api.api_endpoint.APIEndpoint()
app.register_blueprint(zoe_api.rest_api.api_init(api_endpoint))
app.register_blueprint(zoe_api.web.web_init(api_endpoint))
app_settings = {
'static_path': os.path.join(os.path.dirname(__file__), "web", "static"),
'debug': args.debug
}
app = Application(zoe_api.web.web_init(api_endpoint) + zoe_api.rest_api.api_init(api_endpoint), **app_settings)
http_server = HTTPServer(WSGIContainer(app))
http_server.listen(args.listen_port, args.listen_address)
ioloop = IOLoop.instance()
log.info("Starting HTTP server...")
http_server = HTTPServer(app)
http_server.bind(args.listen_port, args.listen_address)
http_server.start(num_processes=1)
retry_cb = PeriodicCallback(api_endpoint.retry_submit_error_executions, 30000)
retry_cb.start()
try:
ioloop.start()
IOLoop.current().start()
except KeyboardInterrupt:
print("CTRL-C detected, terminating")
......
......@@ -15,11 +15,9 @@
"""RESTful Flask API definition."""
import sys
import pkgutil
from typing import List
from flask import Blueprint
from flask_restful import Api
import tornado.web
from zoe_api.rest_api.execution import ExecutionAPI, ExecutionCollectionAPI, ExecutionDeleteAPI
from zoe_api.rest_api.info import InfoAPI
......@@ -31,31 +29,23 @@ from zoe_lib.version import ZOE_API_VERSION
API_PATH = '/api/' + ZOE_API_VERSION
def api_init(api_endpoint) -> Blueprint:
def api_init(api_endpoint) -> List[tornado.web.URLSpec]:
"""Initialize the API"""
api_bp = Blueprint('api', __name__)
api = Api(api_bp, catch_all_404s=True)
route_args = {
'api_endpoint': api_endpoint
}
api.add_resource(InfoAPI, API_PATH + '/info', resource_class_kwargs={'api_endpoint': api_endpoint})
api.add_resource(ExecutionAPI, API_PATH + '/execution/<int:execution_id>', resource_class_kwargs={'api_endpoint': api_endpoint})
api.add_resource(ExecutionDeleteAPI, API_PATH + '/execution/delete/<int:execution_id>', resource_class_kwargs={'api_endpoint': api_endpoint})
api.add_resource(ExecutionCollectionAPI, API_PATH + '/execution', resource_class_kwargs={'api_endpoint': api_endpoint})
api.add_resource(ServiceAPI, API_PATH + '/service/<int:service_id>', resource_class_kwargs={'api_endpoint': api_endpoint})
api.add_resource(ServiceLogsAPI, API_PATH + '/service/logs/<int:service_id>', resource_class_kwargs={'api_endpoint': api_endpoint})
api_routes = [
tornado.web.url(API_PATH + r'/info', InfoAPI, route_args),
api.add_resource(DiscoveryAPI, API_PATH + '/discovery/by_group/<int:execution_id>/<service_group>', resource_class_kwargs={'api_endpoint': api_endpoint})
tornado.web.url(API_PATH + r'/execution/([0-9]+)', ExecutionAPI, route_args),
tornado.web.url(API_PATH + r'/execution/delete/([0-9]+)', ExecutionDeleteAPI, route_args),
tornado.web.url(API_PATH + r'/execution', ExecutionCollectionAPI, route_args),
return api_bp
tornado.web.url(API_PATH + r'/service/([0-9]+)', ServiceAPI, route_args),
tornado.web.url(API_PATH + r'/service/logs/([0-9]+)', ServiceLogsAPI, route_args),
# Work around a Python 3.4.0 bug that affects Flask
if sys.version_info == (3, 4, 0, 'final', 0):
ORIG_LOADER = pkgutil.get_loader
tornado.web.url(API_PATH + r'/discovery/by_group/([0-9]+)/([a-z0-9A-Z\-]+)', DiscoveryAPI, route_args)
]
def get_loader(name):
"""Wrap the original loader to catch a buggy exception."""
try:
return ORIG_LOADER(name)
except AttributeError:
pass
pkgutil.get_loader = get_loader
return api_routes
......@@ -15,20 +15,23 @@
"""The Discovery API endpoint."""
from flask_restful import Resource
from tornado.web import RequestHandler
import zoe_api.api_endpoint
from zoe_api.api_endpoint import APIEndpoint
from zoe_api.rest_api.utils import catch_exceptions
class DiscoveryAPI(Resource):
class DiscoveryAPI(RequestHandler):
"""The Discovery API endpoint."""
def __init__(self, api_endpoint: zoe_api.api_endpoint.APIEndpoint) -> None:
self.api_endpoint = api_endpoint
def initialize(self, **kwargs):
"""Initializes the request handler."""
self.api_endpoint = kwargs['api_endpoint'] # type: APIEndpoint
@catch_exceptions
def get(self, execution_id: int, service_group: str):
"""HTTP GET method."""
self.api_endpoint.execution_by_id(0, 'admin', execution_id)
if service_group != 'all':
services = self.api_endpoint.service_list(0, 'admin', service_group=service_group, execution_id=execution_id)
else:
......@@ -39,4 +42,4 @@ class DiscoveryAPI(Resource):
'dns_names': [s.dns_name for s in services]
}
return ret
self.write(ret)
......@@ -15,28 +15,29 @@
"""The Execution API endpoints."""
from flask_restful import Resource, request
from werkzeug.exceptions import BadRequest
from tornado.web import RequestHandler
import tornado.escape
from zoe_api.rest_api.utils import catch_exceptions, get_auth
import zoe_api.exceptions
import zoe_api.api_endpoint
from zoe_api.api_endpoint import APIEndpoint
class ExecutionAPI(Resource):
class ExecutionAPI(RequestHandler):
"""The Execution API endpoint."""
def __init__(self, api_endpoint: zoe_api.api_endpoint.APIEndpoint) -> None:
self.api_endpoint = api_endpoint
def initialize(self, **kwargs):
"""Initializes the request handler."""
self.api_endpoint = kwargs['api_endpoint'] # type: APIEndpoint
@catch_exceptions
def get(self, execution_id):
"""GET a single execution by its ID."""
uid, role = get_auth(request)
uid, role = get_auth(self)
e = self.api_endpoint.execution_by_id(uid, role, execution_id)
return e.serialize()
self.write(e.serialize())
@catch_exceptions
def delete(self, execution_id: int):
......@@ -46,20 +47,21 @@ class ExecutionAPI(Resource):
:param execution_id: the execution to be terminated
:return:
"""
uid, role = get_auth(request)
uid, role = get_auth(self)
success, message = self.api_endpoint.execution_terminate(uid, role, execution_id)
if not success:
raise zoe_api.exceptions.ZoeRestAPIException(message, 400)
return '', 204
self.set_status(204)
class ExecutionDeleteAPI(Resource):
class ExecutionDeleteAPI(RequestHandler):
"""The ExecutionDelete API endpoints."""
def __init__(self, api_endpoint: zoe_api.api_endpoint.APIEndpoint) -> None:
self.api_endpoint = api_endpoint
def initialize(self, **kwargs):
"""Initializes the request handler."""
self.api_endpoint = kwargs['api_endpoint'] # type: APIEndpoint
@catch_exceptions
def delete(self, execution_id: int):
......@@ -69,20 +71,21 @@ class ExecutionDeleteAPI(Resource):
:param execution_id: the execution to be deleted
:return:
"""
uid, role = get_auth(request)
uid, role = get_auth(self)
success, message = self.api_endpoint.execution_delete(uid, role, execution_id)
if not success:
raise zoe_api.exceptions.ZoeRestAPIException(message, 400)
return '', 204
self.set_status(204)
class ExecutionCollectionAPI(Resource):
class ExecutionCollectionAPI(RequestHandler):
"""The Execution Collection API endpoints."""
def __init__(self, api_endpoint: zoe_api.api_endpoint.APIEndpoint) -> None:
self.api_endpoint = api_endpoint
def initialize(self, **kwargs):
"""Initializes the request handler."""
self.api_endpoint = kwargs['api_endpoint'] # type: APIEndpoint
@catch_exceptions
def get(self):
......@@ -91,10 +94,10 @@ class ExecutionCollectionAPI(Resource):
:return:
"""
uid, role = get_auth(request)
uid, role = get_auth(self)
execs = self.api_endpoint.execution_list(uid, role)
return [e.serialize() for e in execs]
self.write(dict([(e.id, e.serialize()) for e in execs]))
@catch_exceptions
def post(self):
......@@ -102,11 +105,11 @@ class ExecutionCollectionAPI(Resource):
Starts an execution, given an application description. Takes a JSON object.
:return: the new execution_id
"""
uid, role = get_auth(request)
uid, role = get_auth(self)
try:
data = request.get_json()
except BadRequest:
data = tornado.escape.json_decode(self.request.body)
except ValueError:
raise zoe_api.exceptions.ZoeRestAPIException('Error decoding JSON data')
application_description = data['application']
......@@ -114,4 +117,5 @@ class ExecutionCollectionAPI(Resource):
new_id = self.api_endpoint.execution_start(uid, role, exec_name, application_description)
return {'execution_id': new_id}, 201
self.set_status(201)
self.write({'execution_id': new_id})
......@@ -15,18 +15,19 @@
"""The Info API endpoint."""
from flask_restful import Resource
from tornado.web import RequestHandler
import zoe_api.api_endpoint
from zoe_api.rest_api.utils import catch_exceptions
from zoe_lib.config import get_conf
from zoe_lib.version import ZOE_API_VERSION, ZOE_APPLICATION_FORMAT_VERSION, ZOE_VERSION
class InfoAPI(Resource):
class InfoAPI(RequestHandler):
"""The Info API endpoint."""
def __init__(self, api_endpoint: zoe_api.api_endpoint.APIEndpoint) -> None:
self.api_endpoint = api_endpoint
def initialize(self, **kwargs):
"""Initializes the request handler."""
self.api_endpoint = kwargs['api_endpoint'] # type: APIEndpoint
@catch_exceptions
def get(self):
......@@ -38,4 +39,4 @@ class InfoAPI(Resource):
'deployment_name': get_conf().deployment_name
}
return ret
self.write(ret)
......@@ -17,46 +17,66 @@
import logging
from flask_restful import Resource, request
from flask import Response
from tornado.web import RequestHandler
import tornado.gen
from zoe_api.rest_api.utils import catch_exceptions, get_auth
import zoe_api.api_endpoint
from zoe_api.api_endpoint import APIEndpoint
log = logging.getLogger(__name__)
class ServiceAPI(Resource):
class ServiceAPI(RequestHandler):
"""The Service API endpoint."""
def __init__(self, api_endpoint: zoe_api.api_endpoint.APIEndpoint) -> None:
self.api_endpoint = api_endpoint
def initialize(self, **kwargs):
"""Initializes the request handler."""
self.api_endpoint = kwargs['api_endpoint'] # type: APIEndpoint
@catch_exceptions
def get(self, service_id) -> dict:
"""HTTP GET method."""
uid, role = get_auth(request)
uid, role = get_auth(self)
service = self.api_endpoint.service_by_id(uid, role, service_id)
return service.serialize()
self.write(service.serialize())
class ServiceLogsAPI(Resource):
class ServiceLogsAPI(RequestHandler):
"""The Service logs API endpoint."""
def __init__(self, api_endpoint: zoe_api.api_endpoint.APIEndpoint) -> None:
self.api_endpoint = api_endpoint
def initialize(self, **kwargs):
"""Initializes the request handler."""
self.api_endpoint = kwargs['api_endpoint'] # type: APIEndpoint
self.connection_closed = False
def on_connection_close(self):
"""Tornado callback for clients closing the connection."""
self.connection_closed = True
@catch_exceptions
@tornado.gen.engine
def get(self, service_id) -> dict:
"""HTTP GET method."""
uid, role = get_auth(request)
def cb(callback):
if self.connection_closed:
tmp_line = None
else:
tmp_line = next(log_gen)
callback(tmp_line)
uid, role = get_auth(self)
log_gen = self.api_endpoint.service_logs(uid, role, service_id, stream=True)
def flask_stream():
"""Helper function to stream log data."""
for log_line in log_gen:
print(log_line)
yield log_line.decode('utf-8')
while True:
log_line = yield tornado.gen.Task(cb)
if log_line is None:
break
else:
self.write(log_line)
yield self.flush()
return Response(flask_stream(), mimetype='text/plain')
self.finish()
......@@ -15,6 +15,7 @@
"""Utility functions needed by the Zoe REST API."""
import base64
import logging
from zoe_api.exceptions import ZoeRestAPIException, ZoeNotFoundException, ZoeAuthException, ZoeException
......@@ -31,33 +32,44 @@ def catch_exceptions(func):
"""
def func_wrapper(*args, **kwargs):
"""The actual decorator."""
self = args[0]
try:
return func(*args, **kwargs)
except ZoeRestAPIException as e:
if e.status_code != 401:
log.exception(e.message)
return {'message': e.message}, e.status_code, e.headers
self.set_status(e.status_code)
for key, value in e.headers.items():
self.set_header(key, value)
self.write({'message': e.message})
except ZoeNotFoundException as e:
return {'message': e.message}, 404
self.set_status(404)
self.write({'message': e.message})
except ZoeAuthException as e:
return {'message': e.message}, 401
self.set_status(401)
self.write({'message': e.message})
except ZoeException as e:
return {'message': e.message}, 400
self.set_status(400)
self.write({'message': e.message})
except Exception as e:
self.set_status(500)
log.exception(str(e))
return {'message': str(e)}, 500
self.write({'message': str(e)})
return func_wrapper
def get_auth(request):
"""Try to authenticate a request."""
auth = request.authorization
if not auth:
auth_header = request.request.headers.get('Authorization')
if auth_header is None or not auth_header.startswith('Basic '):
raise ZoeRestAPIException('missing or wrong authentication information', 401, {'WWW-Authenticate': 'Basic realm="Login Required"'})
auth_decoded = base64.decodebytes(bytes(auth_header[6:], 'ascii')).decode('utf-8')
username, password = auth_decoded.split(':', 2)
authenticator = LDAPAuthenticator()
uid, role = authenticator.auth(auth.username, auth.password)
uid, role = authenticator.auth(username, password)
if uid is None:
raise ZoeRestAPIException('missing or wrong authentication information', 401, {'WWW-Authenticate': 'Basic realm="Login Required"'})
......
......@@ -15,36 +15,39 @@
"""Flask initialization for the web interface."""
from flask import Blueprint, g
from typing import List
import tornado.web
import zoe_api.web.start
import zoe_api.web.executions
from zoe_lib.version import ZOE_API_VERSION, ZOE_VERSION
def web_init(api_endpoint) -> Blueprint:
def web_init(api_endpoint) -> List[tornado.web.URLSpec]:
"""Flask init for the web interface."""
def before_request():
"""Use the Flask global to hold the api endpoint reference."""
g.api_endpoint = api_endpoint
web_bp = Blueprint('web', __name__, template_folder='templates', static_folder='static')
web_bp.context_processor(inject_version)
# def before_request():
# """Use the Flask global to hold the api endpoint reference."""
# g.api_endpoint = api_endpoint
web_bp.add_url_rule('/', 'index', zoe_api.web.start.index)
web_bp.add_url_rule('/user', 'home_user', zoe_api.web.start.home_user)
web_bp.add_url_rule('/executions/new', 'execution_define', zoe_api.web.executions.execution_define)
web_bp.add_url_rule('/executions/start', 'execution_start', zoe_api.web.executions.execution_start, methods=['POST'])
web_bp.add_url_rule('/executions/restart/<int:execution_id>', 'execution_restart', zoe_api.web.executions.execution_restart)
web_bp.add_url_rule('/executions/terminate/<int:execution_id>', 'execution_terminate', zoe_api.web.executions.execution_terminate)
web_bp.add_url_rule('/executions/delete/<int:execution_id>', 'execution_delete', zoe_api.web.executions.execution_delete)
web_bp.add_url_rule('/executions/inspect/<int:execution_id>', 'execution_inspect', zoe_api.web.executions.execution_inspect)
web_bp.before_request(before_request)
return web_bp
# web_bp = Blueprint('web', __name__, template_folder='templates', static_folder='static')
route_args = {
'api_endpoint': api_endpoint
}
web_routes = [
tornado.web.url(r'/', zoe_api.web.start.index, route_args, name='root'),
tornado.web.url(r'/user', zoe_api.web.start.home_user, route_args, name='home_user'),
tornado.web.url(r'/executions/new', zoe_api.web.executions.execution_define, route_args, name='execution_new'),
tornado.web.url(r'/executions/start', zoe_api.web.executions.execution_start, route_args, name='execution_start'),
tornado.web.url(r'/executions/restart/([0-9]+)', zoe_api.web.executions.execution_restart, route_args, name='execution_restart'),
tornado.web.url(r'/executions/terminate/([0-9]+)', zoe_api.web.executions.execution_terminate, route_args, name='execution_terminate'),
tornado.web.url(r'/executions/delete/([0-9]+)', zoe_api.web.executions.execution_delete, route_args, name='execution_delete'),
tornado.web.url(r'/executions/inspect/([0-9]+)', zoe_api.web.executions.execution_inspect, route_args, name='execution_inspect')
]
return web_routes
def inject_version():
......
......@@ -68,7 +68,7 @@ def exec_list_cmd(args_):
"""List executions"""
exec_api = ZoeExecutionsAPI(utils.zoe_url(), utils.zoe_user(), utils.zoe_pass())
data = exec_api.list()
for e in data:
for e in data.values():
print('Execution {} (User: {}, ID: {}): {}'.format(e['name'], e['user_id'], e['id'], e['status']))
......@@ -131,7 +131,10 @@ def logs_cmd(args):
"""Retrieves and streams the logs of a service."""
service_api = ZoeServiceAPI(utils.zoe_url(), utils.zoe_user(), utils.zoe_pass())
for line in service_api.get_logs(args.service_id):
print(line)
if args.timestamps:
print(line[0], line[1])
else:
print(line[1])
ENV_HELP_TEXT = '''To use this tool you need also to define three environment variables:
......@@ -180,6 +183,7 @@ def process_arguments() -> Tuple[ArgumentParser, Namespace]:
argparser_logs = subparser.add_parser('logs', help="Streams the service logs")
argparser_logs.add_argument('service_id', type=int, help="Service id")
argparser_logs.add_argument('-t', '--timestamps', action='store_true', help="Prefix timestamps for each line")
argparser_logs.set_defaults(func=logs_cmd)
return parser, parser.parse_args()
......
......@@ -16,6 +16,7 @@
"""
This module contains all service-related API calls that a Zoe client can use.
"""
import json
import logging
from zoe_lib.api_base import ZoeAPIBase
......@@ -56,6 +57,7 @@ class ZoeServiceAPI(ZoeAPIBase):
response, status_code = self._rest_get_stream('/service/logs/' + str(container_id))
if status_code == 200:
for line in response.iter_lines():
line = line.decode('utf-8').split(' ', 1)
yield line
elif status_code == 404:
raise ZoeAPIException('service "{}" not found'.format(container_id))
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment