Commit 2e06d70b authored by Daniele Venzano's avatar Daniele Venzano
Browse files

Move the REST API in the web process

parent 18093394
# Copyright (c) 2016, Daniele Venzano
#
# 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 zoe_lib.exceptions import ZoeRestAPIException
def authentication_error():
raise ZoeRestAPIException('Cannot authenticate your request, provide proper credentials', 401, {'WWW-Authenticate': 'Basic realm="Login Required"'})
def authenticate(request, state):
auth = request.authorization
if not auth:
authentication_error()
user = state.get_one('user', name=auth.username)
if user is None:
authentication_error()
if not user.verify_password(auth.password):
authentication_error()
else:
return user
# Copyright (c) 2016, Daniele Venzano
#
# 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 zoe_lib.exceptions import ZoeRestAPIException
import zoe_master.rest_api.utils
def authorization_error():
raise ZoeRestAPIException('You do not have necessary permissions for the resource', 403)
def is_authorized(user, obj, action):
# Zoeadmin and admins can do anything they want
if user.role == "admin":
return True
# Anyone can work on his own objects
if obj.owner == user:
return True
authorization_error()
QUOTA_MAX_APPS_GUESTS = 1
QUOTA_MAX_MEM_GUESTS = 16 * (2**30)
QUOTA_MAX_PROCS_GUESTS = 5
def check_quota(user):
if user.role == "guest" and zoe_master.rest_api.utils.user_has_active_executions(user):
raise ZoeRestAPIException('Quota exceeded', 402)
return True
# Copyright (c) 2016, Daniele Venzano
#
# 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 time
from werkzeug.exceptions import BadRequest
from flask_restful import Resource, request
from zoe_lib.exceptions import ZoeException, ZoeRestAPIException
from zoe_master.rest_api.utils import catch_exceptions, user_has_active_executions
from zoe_master.state.manager import StateManager
from zoe_master.platform_manager import PlatformManager
from zoe_master.rest_api.auth.authentication import authenticate
from zoe_master.rest_api.auth.authorization import is_authorized
from zoe_master.state.user import User
from zoe_master.config import singletons
import zoe_master.workspace.base
class UserAPI(Resource):
"""
:type state: StateManager
:type platform: PlatformManager
"""
def __init__(self, **kwargs):
self.state = kwargs['state']
self.platform = kwargs['platform']
@catch_exceptions
def get(self, user_name):
start_time = time.time()
calling_user = authenticate(request, self.state)
u = self.state.get_one('user', name=user_name)
if u is None:
raise ZoeRestAPIException('No such user', 404)
is_authorized(calling_user, u, 'get')
d = u.to_dict(checkpoint=False)
singletons['metric'].metric_api_call(start_time, 'user', 'get', calling_user)
return d, 200
@catch_exceptions
def delete(self, user_name):
start_time = time.time()
calling_user = authenticate(request, self.state)
user = self.state.get_one('user', name=user_name)
if user is None:
raise ZoeRestAPIException('No such user', 404)
is_authorized(calling_user, user, 'delete')
if user_has_active_executions(user):
raise ZoeRestAPIException('User has running executions, cannot delete')
self.state.delete('user', user.id)
self.state.state_updated()
singletons['metric'].metric_api_call(start_time, 'user', 'delete', calling_user)
return '', 204
class UserCollectionAPI(Resource):
"""
:type state: StateManager
:type platform: PlatformManager
"""
def __init__(self, **kwargs):
self.state = kwargs['state']
self.platform = kwargs['platform']
@catch_exceptions
def post(self):
"""
Create a new user
:return:
"""
start_time = time.time()
calling_user = authenticate(request, self.state)
try:
data = request.get_json()
except BadRequest:
raise ZoeRestAPIException('Error decoding JSON data')
user = User(self.state)
try:
user.from_dict(data, checkpoint=False)
except ZoeException as e:
raise ZoeRestAPIException(e.value)
is_authorized(calling_user, user, 'create')
if len(self.state.get('user', name=user.name)) > 0:
raise ZoeRestAPIException('User name already registered')
user.set_password(data['password'])
user.id = self.state.gen_id()
self.state.new('user', user)
for wks in singletons['workspace_managers']:
assert isinstance(wks, zoe_master.workspace.base.ZoeWorkspaceBase)
wks.create(user)
self.state.state_updated()
singletons['metric'].metric_api_call(start_time, 'user', 'post', calling_user)
return {"user": user.to_dict(checkpoint=False)}, 201
......@@ -14,37 +14,30 @@
# limitations under the License.
import sys
from flask import Flask
from flask import Blueprint
from flask_restful import Api
from zoe_web.rest_api.execution import ExecutionAPI, ExecutionCollectionAPI
from zoe_web.rest_api.info import InfoAPI
from zoe_web.rest_api.service import ServiceAPI
from zoe_master.rest_api.user import UserAPI, UserCollectionAPI
from zoe_master.rest_api.execution import ExecutionAPI, ExecutionCollectionAPI
from zoe_master.rest_api.service import ServiceAPI
from zoe_master.rest_api.query import QueryAPI
from zoe_master.rest_api.info import InfoAPI
from zoe_lib.version import ZOE_API_VERSION
from zoe_web.rest_api.query import QueryAPI
API_PATH = '/api/' + ZOE_API_VERSION
def init(state, platform) -> Flask:
app = Flask("Zoe REST API")
api = Api(app, catch_all_404s=True)
args = {
'state': state,
'platform': platform
}
def api_init() -> Blueprint:
api_bp = Blueprint('api', __name__)
api = Api(api_bp, catch_all_404s=True)
api.add_resource(InfoAPI, API_PATH + '/info', resource_class_kwargs=args)
api.add_resource(UserAPI, API_PATH + '/user/<user_name>', resource_class_kwargs=args)
api.add_resource(UserCollectionAPI, API_PATH + '/user', resource_class_kwargs=args)
api.add_resource(ExecutionAPI, API_PATH + '/execution/<int:execution_id>', resource_class_kwargs=args)
api.add_resource(ExecutionCollectionAPI, API_PATH + '/execution', resource_class_kwargs=args)
api.add_resource(ServiceAPI, API_PATH + '/service/<int:container_id>', resource_class_kwargs=args)
api.add_resource(QueryAPI, API_PATH + '/query', resource_class_kwargs=args)
api.add_resource(InfoAPI, API_PATH + '/info')
api.add_resource(ExecutionAPI, API_PATH + '/execution/<int:execution_id>')
api.add_resource(ExecutionCollectionAPI, API_PATH + '/execution')
api.add_resource(ServiceAPI, API_PATH + '/service/<int:container_id>')
api.add_resource(QueryAPI, API_PATH + '/query')
return app
return api_bp
# Work around a Python 3.4.0 bug that affects Flask
if sys.version_info == (3, 4, 0, 'final', 0):
......
......@@ -13,43 +13,30 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import time
from werkzeug.exceptions import BadRequest
import re
from flask_restful import Resource, request
from werkzeug.exceptions import BadRequest
from zoe_lib.exceptions import ZoeException, ZoeRestAPIException
from zoe_master.rest_api.utils import catch_exceptions
from zoe_master.state.manager import StateManager
from zoe_master.platform_manager import PlatformManager
from zoe_master.state.execution import Execution
from zoe_master.rest_api.auth.authentication import authenticate
from zoe_master.rest_api.auth.authorization import is_authorized, check_quota
from zoe_master.config import singletons
import zoe_lib.exceptions
import zoe_lib.applications
import zoe_lib.sql_manager
from zoe_web.rest_api.utils import catch_exceptions, get_auth
import zoe_web.exceptions
import zoe_web.config as config
import zoe_web.api_endpoint
class ExecutionAPI(Resource):
"""
:type state: StateManager
:type platform: PlatformManager
"""
def __init__(self, **kwargs):
self.state = kwargs['state']
self.platform = kwargs['platform']
class ExecutionAPI(Resource):
@catch_exceptions
def get(self, execution_id):
start_time = time.time()
calling_user = authenticate(request, self.state)
e = self.state.get_one('execution', id=execution_id)
if e is None:
raise ZoeRestAPIException('No such execution', 404)
uid, role = get_auth(request)
is_authorized(calling_user, e, 'get')
ret = e.to_dict(checkpoint=False)
assert isinstance(config.api_endpoint, zoe_web.api_endpoint.APIEndpoint)
e = config.api_endpoint.execution_by_id(uid, role, execution_id)
singletons['metric'].metric_api_call(start_time, 'execution', 'get', calling_user)
return ret
return e.serialize()
@catch_exceptions
def delete(self, execution_id: int):
......@@ -59,33 +46,17 @@ class ExecutionAPI(Resource):
:param execution_id: the execution to be deleted
:return:
"""
start_time = time.time()
calling_user = authenticate(request, self.state)
e = self.state.get_one('execution', id=execution_id)
if e is None:
raise ZoeRestAPIException('No such execution', 404)
is_authorized(calling_user, e, 'delete')
uid, role = get_auth(request)
if e.is_active():
self.platform.execution_terminate(e, reason='terminated')
assert isinstance(config.api_endpoint, zoe_web.api_endpoint.APIEndpoint)
success, message = config.api_endpoint.execution_terminate(uid, role, execution_id)
if not success:
raise zoe_web.exceptions.ZoeRestAPIException(message, 400)
self.state.state_updated()
singletons['metric'].metric_api_call(start_time, 'execution', 'delete', calling_user)
return '', 204
class ExecutionCollectionAPI(Resource):
"""
:type state: StateManager
:type platform: PlatformManager
"""
def __init__(self, **kwargs):
self.state = kwargs['state']
self.platform = kwargs['platform']
@catch_exceptions
def get(self):
"""
......@@ -93,19 +64,11 @@ class ExecutionCollectionAPI(Resource):
:return:
"""
start_time = time.time()
calling_user = authenticate(request, self.state)
execs = self.state.get('execution')
ret = []
for e in execs:
try:
is_authorized(calling_user, e, "get")
except ZoeRestAPIException:
continue
else:
ret.append(e.to_dict(checkpoint=False))
singletons['metric'].metric_api_call(start_time, 'execution', 'list', calling_user)
return ret
uid, role = get_auth(request)
assert isinstance(config.api_endpoint, zoe_web.api_endpoint.APIEndpoint)
execs = config.api_endpoint.execution_list(uid, role)
return [e.serialize() for e in execs]
@catch_exceptions
def post(self):
......@@ -113,33 +76,18 @@ class ExecutionCollectionAPI(Resource):
Starts an execution, given an application description. Takes a JSON object.
:return: the new execution_id
"""
start_time = time.time()
calling_user = authenticate(request, self.state)
uid, role = get_auth(request)
assert isinstance(config.api_endpoint, zoe_web.api_endpoint.APIEndpoint)
try:
data = request.get_json()
except BadRequest:
raise ZoeRestAPIException('Error decoding JSON data')
execution = Execution(self.state)
data['user_id'] = calling_user.id
try:
execution.from_dict(data, checkpoint=False)
except ZoeException as e:
raise ZoeRestAPIException(e.value)
is_authorized(calling_user, execution, 'create')
old_exec = self.state.get_one('execution', name=execution.name, owner=calling_user)
if old_exec is not None:
if old_exec.is_active():
raise ZoeRestAPIException('An execution with the same name is already running')
else:
self.state.delete('execution', old_exec.id)
raise zoe_web.exceptions.ZoeRestAPIException('Error decoding JSON data')
check_quota(calling_user)
application_description = data['application']
exec_name = data['name']
self.platform.execution_submit(execution)
new_id = config.api_endpoint.execution_start(uid, role, exec_name, application_description)
singletons['metric'].metric_api_call(start_time, 'execution', 'start', calling_user)
return {'execution_id': execution.id}, 201
return {'execution_id': new_id}, 201
......@@ -13,32 +13,16 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import time
from flask_restful import Resource, request
from flask_restful import Resource
from zoe_lib.version import ZOE_API_VERSION, ZOE_APPLICATION_FORMAT_VERSION, ZOE_VERSION
from zoe_master.state.manager import StateManager
from zoe_master.platform_manager import PlatformManager
from zoe_master.rest_api.utils import catch_exceptions
from zoe_master.rest_api.auth.authentication import authenticate
from zoe_master.config import singletons, get_conf
from zoe_master.config import get_conf
from zoe_web.rest_api.utils import catch_exceptions
class InfoAPI(Resource):
"""
:type state: StateManager
:type platform: PlatformManager
"""
def __init__(self, **kwargs):
self.state = kwargs['state']
self.platform = kwargs['platform']
@catch_exceptions
def get(self):
start_time = time.time()
calling_user = authenticate(request, self.state)
ret = {
'version': ZOE_VERSION,
'api_version': ZOE_API_VERSION,
......@@ -46,5 +30,4 @@ class InfoAPI(Resource):
'deployment_name': get_conf().deployment_name
}
singletons['metric'].metric_api_call(start_time, 'info', 'get', calling_user)
return ret
......@@ -13,32 +13,20 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import time
from werkzeug.exceptions import BadRequest
from flask_restful import Resource, request
from werkzeug.exceptions import BadRequest
from zoe_lib.exceptions import ZoeRestAPIException
from zoe_master.state.manager import StateManager
from zoe_master.platform_manager import PlatformManager
from zoe_master.rest_api.utils import catch_exceptions
from zoe_master.rest_api.auth.authentication import authenticate
from zoe_master.config import singletons
from zoe_web.exceptions import ZoeRestAPIException
import zoe_web.config as config
import zoe_web.api_endpoint
from zoe_web.rest_api.utils import catch_exceptions, get_auth
class QueryAPI(Resource):
"""
:type state: StateManager
:type platform: PlatformManager
"""
def __init__(self, **kwargs):
self.state = kwargs['state']
self.platform = kwargs['platform']
@catch_exceptions
def post(self):
start_time = time.time()
calling_user = authenticate(request, self.state)
uid, role = get_auth(request)
assert isinstance(config.api_endpoint, zoe_web.api_endpoint.APIEndpoint)
try:
data = request.get_json()
......@@ -57,33 +45,15 @@ class QueryAPI(Resource):
if not isinstance(filters, dict):
raise ZoeRestAPIException('query filters should be a dictionary of {attribute: requested_value} entries')
if not calling_user.can_see_non_owner_objects():
filters['owner'] = calling_user
if what == 'stats_swarm':
ret = singletons['stats_manager'].swarm_stats
ret = {'stats': ret.to_dict()}
elif what == 'stats_scheduler':
ret = self.platform.scheduler_stats()
ret = {'stats': ret.to_dict()}
if what == 'stats_scheduler':
# TODO
ret = None
elif what == 'execution':
if role != 'admin':
filters['user_id'] = uid
execs = config.api_endpoint.execution_list(uid, role, filters)
return [x.serialize() for x in execs]
else:
ret = self.state.get(what, **filters)
ret = [o.to_dict(checkpoint=False) for o in ret]
singletons['metric'].metric_api_call(start_time, 'query', what, calling_user)
return ret
def _get_container_logs(self, c_list):
logs = []
for c in c_list:
ret = self.platform.log_get(c.id)
logs.append(ret)
return logs
raise ZoeRestAPIException('unknown query {}'.format(what))
def _get_container_stats(self, c_list):
stats = []
for c in c_list:
s = self.platform.container_stats(c.id)
stats.append(s)
return stats
return ret
......@@ -15,28 +15,17 @@
import logging
import time
from flask_restful import Resource, request
from zoe_lib.exceptions import ZoeRestAPIException
from zoe_master.state.manager import StateManager
from zoe_master.platform_manager import PlatformManager
from zoe_master.rest_api.utils import catch_exceptions
from zoe_master.rest_api.auth.authentication import authenticate
from zoe_master.rest_api.auth.authorization import is_authorized
from zoe_master.config import singletons
from zoe_web.config import singletons
from zoe_web.rest_api.utils import catch_exceptions
log = logging.getLogger(__name__)
class ServiceAPI(Resource):
"""
:type state: StateManager
:type platform: PlatformManager
"""
def __init__(self, **kwargs):
self.state = kwargs['state']
self.platform = kwargs['platform']
@catch_exceptions
def get(self, container_id):
start_time = time.time()
......
......@@ -15,8 +15,8 @@
import logging
from zoe_lib.exceptions import ZoeRestAPIException
from zoe_master.state.user import User
from zoe_web.exceptions import ZoeRestAPIException, ZoeNotFoundException, ZoeAuthException, ZoeException
from zoe_web.auth.ldap import LDAPAuthenticator
log = logging.getLogger(__name__)
......@@ -32,8 +32,14 @@ def catch_exceptions(func):
return func(*args, **kwargs)
except ZoeRestAPIException as e:
if e.status_code != 401:
log.exception(e.value)
return {'message': e.value}, e.status_code, e.headers
log.exception(e.message)
return {'message': e.message}, e.status_code, e.headers
except ZoeNotFoundException as e:
return {'message': e.message}, 404
except ZoeAuthException as e:
return {'message': e.message}, 401
except ZoeException as e:
return {'message': e.message}, 400
except Exception as e:
log.exception(str(e))
return {'message': str(e)}, 500
......@@ -41,8 +47,14 @@ def catch_exceptions(func):
return func_wrapper