mirror of
https://github.com/YunoHost/moulinette.git
synced 2024-09-03 20:06:31 +02:00
Add support for Authenticators in the API
* Update the API to support authenticators and session token * Add a function to clean sessions * Make the API fit in better with actions map * Minor changes on test namespace
This commit is contained in:
parent
5da9f6add8
commit
3fae8bf1ff
7 changed files with 320 additions and 178 deletions
|
@ -41,5 +41,5 @@ if __name__ == '__main__':
|
|||
# TODO: Add log argument
|
||||
|
||||
# Rune the server
|
||||
api(['yunohost'], 6787, {('GET', '/installed'): is_installed}, use_cache)
|
||||
api(['yunohost', 'test'], 6787, {('GET', '/installed'): is_installed}, use_cache)
|
||||
sys.exit(0)
|
||||
|
|
|
@ -19,6 +19,13 @@ _global:
|
|||
parameters:
|
||||
uri: ldap://localhost:389
|
||||
base_dn: dc=yunohost,dc=org
|
||||
test-profile:
|
||||
vendor: ldap
|
||||
help: Admin Password (profile)
|
||||
parameters:
|
||||
uri: ldap://localhost:389
|
||||
base_dn: dc=yunohost,dc=org
|
||||
user_rdn: cn=admin
|
||||
argument_auth: true
|
||||
|
||||
#############################
|
||||
|
@ -34,6 +41,11 @@ test:
|
|||
api: GET /test/auth
|
||||
configuration:
|
||||
authenticate: all
|
||||
auth-profile:
|
||||
api: GET /test/auth-profile
|
||||
configuration:
|
||||
authenticate: all
|
||||
authenticator: test-profile
|
||||
auth-cli:
|
||||
api: GET /test/auth-cli
|
||||
configuration:
|
||||
|
|
|
@ -1,12 +1,19 @@
|
|||
|
||||
def test_non_auth():
|
||||
print('non-auth')
|
||||
return {'action': 'non-auth'}
|
||||
|
||||
def test_auth(auth):
|
||||
print('[default] / all / auth: %r' % auth)
|
||||
return {'action': 'auth',
|
||||
'authenticator': 'default', 'authenticate': 'all'}
|
||||
|
||||
def test_auth_profile(auth):
|
||||
return {'action': 'auth-profile',
|
||||
'authenticator': 'test-profile', 'authenticate': 'all'}
|
||||
|
||||
def test_auth_cli():
|
||||
print('[default] / cli')
|
||||
return {'action': 'auth-cli',
|
||||
'authenticator': 'default', 'authenticate': ['cli']}
|
||||
|
||||
def test_anonymous():
|
||||
print('[ldap-anonymous] / all')
|
||||
return {'action': 'anonymous',
|
||||
'authenticator': 'ldap-anonymous', 'authenticate': 'all'}
|
||||
|
|
|
@ -69,21 +69,20 @@ def api(namespaces, port, routes={}, use_cache=True):
|
|||
|
||||
Keyword arguments:
|
||||
- namespaces -- The list of namespaces to use
|
||||
- port -- Port to run on
|
||||
- port -- Port number to run on
|
||||
- routes -- A dict of additional routes to add in the form of
|
||||
{(method, uri): callback}
|
||||
- use_cache -- False if it should parse the actions map file
|
||||
instead of using the cached one
|
||||
|
||||
"""
|
||||
from bottle import run
|
||||
from .actionsmap import ActionsMap
|
||||
from .interface.api import MoulinetteAPI
|
||||
|
||||
amap = ActionsMap('api', namespaces, use_cache)
|
||||
moulinette = MoulinetteAPI(amap, routes)
|
||||
|
||||
run(moulinette.app, port=port)
|
||||
moulinette.run(port)
|
||||
|
||||
def cli(namespaces, args, use_cache=True):
|
||||
"""Command line interface
|
||||
|
|
|
@ -48,7 +48,7 @@ class _AMapSignals(object):
|
|||
"""The list of available signals"""
|
||||
signals = { 'authenticate', 'prompt' }
|
||||
|
||||
def authenticate(self, authenticator, name, help):
|
||||
def authenticate(self, authenticator, name, help, vendor=None):
|
||||
"""Process the authentication
|
||||
|
||||
Attempt to authenticate to the given authenticator and return
|
||||
|
@ -60,6 +60,7 @@ class _AMapSignals(object):
|
|||
- authenticator -- The authenticator to use
|
||||
- name -- The authenticator name in the actions map
|
||||
- help -- A help message for the authenticator
|
||||
- vendor -- Not expected (TODO: Remove it)
|
||||
|
||||
Returns:
|
||||
The authenticator object
|
||||
|
@ -227,14 +228,10 @@ class _AMapParser(object):
|
|||
- profile -- The profile of the configuration
|
||||
|
||||
"""
|
||||
try:
|
||||
if name == 'authenticator':
|
||||
value = self.global_conf[name][profile]
|
||||
else:
|
||||
value = self.global_conf[name]
|
||||
except KeyError:
|
||||
return None
|
||||
else:
|
||||
return self._format_conf(name, value)
|
||||
|
||||
def set_global_conf(self, configuration):
|
||||
|
@ -366,13 +363,13 @@ class _AMapParser(object):
|
|||
"""
|
||||
if name == 'authenticator' and value:
|
||||
auth_conf, auth_params = value
|
||||
auth_vendor = auth_conf.pop('vendor')
|
||||
|
||||
# Return authenticator configuration and an instanciator for
|
||||
# it as a 2-tuple
|
||||
return (auth_conf,
|
||||
lambda: init_authenticator(auth_conf['name'],
|
||||
auth_vendor, **auth_params))
|
||||
auth_conf['vendor'],
|
||||
**auth_params))
|
||||
|
||||
return value
|
||||
|
||||
|
@ -486,7 +483,7 @@ class _HTTPArgumentParser(object):
|
|||
|
||||
return action
|
||||
|
||||
def parse_args(self, args):
|
||||
def parse_args(self, args={}, namespace=None):
|
||||
arg_strings = []
|
||||
|
||||
## Append an argument to the current one
|
||||
|
@ -514,7 +511,7 @@ class _HTTPArgumentParser(object):
|
|||
for dest, opt in self._optional.items():
|
||||
if dest in args:
|
||||
arg_strings = append(arg_strings, args[dest], opt[0])
|
||||
return self._parser.parse_args(arg_strings)
|
||||
return self._parser.parse_args(arg_strings, namespace)
|
||||
|
||||
def _error(self, message):
|
||||
# TODO: Raise a proper exception
|
||||
|
@ -596,16 +593,28 @@ class APIAMapParser(_AMapParser):
|
|||
"""Parse arguments
|
||||
|
||||
Keyword arguments:
|
||||
- route -- The action route (e.g. 'GET /' )
|
||||
- route -- The action route as a 2-tuple (method, path)
|
||||
|
||||
"""
|
||||
# Retrieve the parser for the route
|
||||
if route not in self.routes:
|
||||
raise MoulinetteError(22, "No parser for '%s %s' found" % key)
|
||||
ret = argparse.Namespace()
|
||||
|
||||
# TODO: Implement authentication
|
||||
# Perform authentication if needed
|
||||
if self.get_conf(route, 'authenticate'):
|
||||
auth_conf, klass = self.get_conf(route, 'authenticator')
|
||||
|
||||
return self._parsers[route].parse_args(args)
|
||||
# TODO: Catch errors
|
||||
auth = shandler.authenticate(klass(), **auth_conf)
|
||||
if not auth.is_authenticated:
|
||||
# TODO: Set proper error code
|
||||
raise MoulinetteError(1, _("This action need authentication"))
|
||||
if self.get_conf(route, 'argument_auth') and \
|
||||
self.get_conf(route, 'authenticate') == 'all':
|
||||
ret.auth = auth
|
||||
|
||||
return self._parsers[route].parse_args(args, ret)
|
||||
|
||||
"""
|
||||
The dict of interfaces names and their associated parser class.
|
||||
|
@ -906,6 +915,26 @@ class ActionsMap(object):
|
|||
"""Return the instance of the interface's actions map parser"""
|
||||
return self._parser
|
||||
|
||||
def get_authenticator(self, profile='default'):
|
||||
"""Get an authenticator instance
|
||||
|
||||
Retrieve the authenticator for the given profile and return a
|
||||
new instance.
|
||||
|
||||
Keyword arguments:
|
||||
- profile -- An authenticator profile name
|
||||
|
||||
Returns:
|
||||
A new _BaseAuthenticator derived instance
|
||||
|
||||
"""
|
||||
try:
|
||||
auth = self.parser.get_global_conf('authenticator', profile)[1]
|
||||
except KeyError:
|
||||
raise MoulinetteError(167, _("Unknown authenticator profile '%s'") % profile)
|
||||
else:
|
||||
return auth()
|
||||
|
||||
def connect(self, signal, handler):
|
||||
"""Connect a signal to a handler
|
||||
|
||||
|
|
|
@ -316,6 +316,17 @@ def init_authenticator(_name, _vendor, **kwargs):
|
|||
if _vendor == 'ldap':
|
||||
return LDAPAuthenticator(name=_name, **kwargs)
|
||||
|
||||
def clean_session(session_id, profiles=[]):
|
||||
sessiondir = pkg.get_cachedir('session')
|
||||
if len(profiles) == 0:
|
||||
profiles = os.listdir(sessiondir)
|
||||
|
||||
for p in profiles:
|
||||
try:
|
||||
os.unlink(os.path.join(sessiondir, p, '%s.asc' % session_id))
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
# Moulinette core classes ----------------------------------------------
|
||||
|
||||
|
|
|
@ -1,147 +1,118 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from bottle import Bottle, request, response, HTTPResponse
|
||||
from beaker.middleware import SessionMiddleware
|
||||
from bottle import run, request, response, Bottle, HTTPResponse
|
||||
from json import dumps as json_encode
|
||||
|
||||
from ..core import MoulinetteError
|
||||
from ..core import MoulinetteError, clean_session
|
||||
from ..helpers import YunoHostError, YunoHostLDAP
|
||||
|
||||
# API helpers ----------------------------------------------------------
|
||||
|
||||
import os
|
||||
import binascii
|
||||
|
||||
def random20():
|
||||
return binascii.hexlify(os.urandom(20)).decode('ascii')
|
||||
|
||||
|
||||
# HTTP Responses -------------------------------------------------------
|
||||
|
||||
class HTTPOKResponse(HTTPResponse):
|
||||
def __init__(self, output=''):
|
||||
super(HTTPOKResponse, self).__init__(output, 200)
|
||||
|
||||
class HTTPBadRequestResponse(HTTPResponse):
|
||||
def __init__(self, output=''):
|
||||
super(HTTPBadRequestResponse, self).__init__(output, 400)
|
||||
|
||||
class HTTPUnauthorizedResponse(HTTPResponse):
|
||||
def __init__(self, output=''):
|
||||
super(HTTPUnauthorizedResponse, self).__init__(output, 401)
|
||||
|
||||
class HTTPErrorResponse(HTTPResponse):
|
||||
def __init__(self, output=''):
|
||||
super(HTTPErrorResponse, self).__init__(output, 500)
|
||||
|
||||
|
||||
# API moulinette interface ---------------------------------------------
|
||||
|
||||
class _APIAuthPlugin(object):
|
||||
"""
|
||||
Manage the authentication for the API access.
|
||||
|
||||
"""
|
||||
name = 'apiauth'
|
||||
api = 2
|
||||
|
||||
def __init__(self):
|
||||
# TODO: Add options (e.g. session type, content type, ...)
|
||||
pass
|
||||
|
||||
@property
|
||||
def app(self):
|
||||
"""Get Bottle application with session integration"""
|
||||
if hasattr(self, '_app'):
|
||||
return self._app
|
||||
raise Exception(_("The APIAuth Plugin is not installed yet."))
|
||||
|
||||
def setup(self, app):
|
||||
"""
|
||||
Setup the plugin and install the session into the app
|
||||
|
||||
Keyword argument:
|
||||
app -- The associated application object
|
||||
|
||||
"""
|
||||
app.route('/login', name='login', method='POST', callback=self.login)
|
||||
app.route('/logout', name='logout', method='GET', callback=self.logout)
|
||||
|
||||
session_opts = {
|
||||
'session.type': 'file',
|
||||
'session.cookie_expires': True,
|
||||
'session.data_dir': pkg.get_cachedir('session'),
|
||||
'session.secure': True
|
||||
}
|
||||
self._app = SessionMiddleware(app, session_opts)
|
||||
|
||||
def apply(self, callback, context):
|
||||
"""
|
||||
Check authentication before executing the route callback
|
||||
|
||||
Keyword argument:
|
||||
callback -- The route callback
|
||||
context -- An instance of Route
|
||||
|
||||
"""
|
||||
# Check the authentication
|
||||
if self._is_authenticated:
|
||||
if context.name == 'login':
|
||||
self.logout()
|
||||
else:
|
||||
return callback
|
||||
|
||||
# Process login route
|
||||
if context.name == 'login':
|
||||
password = request.POST.get('password', None)
|
||||
if password is not None and self.login(password):
|
||||
raise HTTPResponse(status=200)
|
||||
else:
|
||||
raise HTTPResponse(_("Wrong password"), 401)
|
||||
|
||||
# Deny access to the requested route
|
||||
raise HTTPResponse(_("Unauthorized"), 401)
|
||||
|
||||
def login(self, password):
|
||||
"""
|
||||
Attempt to log in with the given password
|
||||
|
||||
Keyword argument:
|
||||
password -- Cleartext password
|
||||
|
||||
"""
|
||||
try: YunoHostLDAP(password=password)
|
||||
except YunoHostError:
|
||||
return False
|
||||
else:
|
||||
session = self._beaker_session
|
||||
session['authenticated'] = True
|
||||
session.save()
|
||||
return True
|
||||
return False
|
||||
|
||||
def logout(self):
|
||||
"""
|
||||
Log out and delete the session
|
||||
|
||||
"""
|
||||
# TODO: Delete the cached session file
|
||||
session = self._beaker_session
|
||||
session.delete()
|
||||
|
||||
|
||||
## Private methods
|
||||
|
||||
@property
|
||||
def _beaker_session(self):
|
||||
"""Get Beaker session"""
|
||||
return request.environ.get('beaker.session')
|
||||
|
||||
@property
|
||||
def _is_authenticated(self):
|
||||
"""Check authentication"""
|
||||
# TODO: Clear the session path on password changing to avoid invalid access
|
||||
if 'authenticated' in self._beaker_session:
|
||||
return True
|
||||
return False
|
||||
|
||||
class _ActionsMapPlugin(object):
|
||||
"""
|
||||
Process action for the request using the actions map.
|
||||
"""Actions map Bottle Plugin
|
||||
|
||||
Process relevant action for the request using the actions map and
|
||||
manage authentication.
|
||||
|
||||
Keyword arguments:
|
||||
- actionsmap -- An ActionsMap instance
|
||||
|
||||
"""
|
||||
name = 'actionsmap'
|
||||
api = 2
|
||||
|
||||
def __init__(self, actionsmap):
|
||||
# Connect signals to handlers
|
||||
actionsmap.connect('authenticate', self._do_authenticate)
|
||||
|
||||
self.actionsmap = actionsmap
|
||||
# TODO: Save and load secrets?
|
||||
self.secrets = {}
|
||||
|
||||
def setup(self, app):
|
||||
"""Setup plugin on the application
|
||||
|
||||
Add routes according to the actions map to the application.
|
||||
|
||||
Keyword arguments:
|
||||
- app -- The application instance
|
||||
|
||||
"""
|
||||
## Login wrapper
|
||||
def _login(callback):
|
||||
def wrapper():
|
||||
kwargs = {}
|
||||
try:
|
||||
kwargs['password'] = request.POST['password']
|
||||
except KeyError:
|
||||
raise HTTPBadRequestResponse(_("Missing password parameter"))
|
||||
try:
|
||||
kwargs['profile'] = request.POST['profile']
|
||||
except KeyError:
|
||||
pass
|
||||
return callback(**kwargs)
|
||||
return wrapper
|
||||
|
||||
## Logout wrapper
|
||||
def _logout(callback):
|
||||
def wrapper():
|
||||
kwargs = {}
|
||||
try:
|
||||
kwargs['profile'] = request.POST.get('profile')
|
||||
except KeyError:
|
||||
pass
|
||||
return callback(**kwargs)
|
||||
return wrapper
|
||||
|
||||
# Append authentication routes
|
||||
app.route('/login', name='login', method='POST',
|
||||
callback=self.login, skip=['actionsmap'], apply=_login)
|
||||
app.route('/logout', name='logout', method='GET',
|
||||
callback=self.logout, skip=['actionsmap'], apply=_logout)
|
||||
|
||||
# Append routes from the actions map
|
||||
for (m, p) in self.actionsmap.parser.routes:
|
||||
app.route(p, method=m, callback=self.process)
|
||||
|
||||
def apply(self, callback, context):
|
||||
"""
|
||||
Process the relevant action for the request
|
||||
"""Apply plugin to the route callback
|
||||
|
||||
Keyword argument:
|
||||
Install a wrapper which replace callback and process the
|
||||
relevant action for the route.
|
||||
|
||||
Keyword arguments:
|
||||
callback -- The route callback
|
||||
context -- An instance of Route
|
||||
|
||||
"""
|
||||
method = request.method
|
||||
uri = context.rule
|
||||
|
||||
def wrapper(*args, **kwargs):
|
||||
# Bring arguments together
|
||||
params = kwargs
|
||||
|
@ -151,14 +122,127 @@ class _ActionsMapPlugin(object):
|
|||
params[k] = v
|
||||
|
||||
# Process the action
|
||||
return self.actionsmap.process(params, route=(method, uri))
|
||||
return callback((request.method, context.rule), params)
|
||||
return wrapper
|
||||
|
||||
|
||||
class MoulinetteAPI(object):
|
||||
## Routes callbacks
|
||||
|
||||
def login(self, password, profile='default'):
|
||||
"""Log in to an authenticator profile
|
||||
|
||||
Attempt to authenticate to a given authenticator profile and
|
||||
register it with the current session - a new one will be created
|
||||
if needed.
|
||||
|
||||
Keyword arguments:
|
||||
- password -- A clear text password
|
||||
- profile -- The authenticator profile name to log in
|
||||
|
||||
"""
|
||||
Initialize a HTTP server which serves the API to access to the
|
||||
moulinette actions.
|
||||
# Retrieve session values
|
||||
s_id = request.get_cookie('session.id') or random20()
|
||||
try:
|
||||
s_secret = self.secrets[s_id]
|
||||
except KeyError:
|
||||
s_hashes = {}
|
||||
else:
|
||||
s_hashes = request.get_cookie('session.hashes',
|
||||
secret=s_secret) or {}
|
||||
s_hash = random20()
|
||||
|
||||
try:
|
||||
# Attempt to authenticate
|
||||
auth = self.actionsmap.get_authenticator(profile)
|
||||
auth(password, token=(s_id, s_hash))
|
||||
except MoulinetteError as e:
|
||||
if len(s_hashes) > 0:
|
||||
try: self.logout(profile)
|
||||
except: pass
|
||||
# TODO: Replace by proper exception
|
||||
if e.code == 13:
|
||||
raise HTTPUnauthorizedResponse(e.message)
|
||||
raise HTTPErrorResponse(e.message)
|
||||
else:
|
||||
# Update dicts with new values
|
||||
s_hashes[profile] = s_hash
|
||||
self.secrets[s_id] = s_secret = random20()
|
||||
|
||||
response.set_cookie('session.id', s_id, secure=True)
|
||||
response.set_cookie('session.hashes', s_hashes, secure=True,
|
||||
secret=s_secret)
|
||||
raise HTTPOKResponse()
|
||||
|
||||
def logout(self, profile=None):
|
||||
"""Log out from an authenticator profile
|
||||
|
||||
Attempt to unregister a given profile - or all by default - from
|
||||
the current session.
|
||||
|
||||
Keyword arguments:
|
||||
- profile -- The authenticator profile name to log out
|
||||
|
||||
"""
|
||||
s_id = request.get_cookie('session.id')
|
||||
try:
|
||||
del self.secrets[s_id]
|
||||
except KeyError:
|
||||
raise HTTPUnauthorizedResponse(_("You are not logged in"))
|
||||
else:
|
||||
# TODO: Clean the session for profile only
|
||||
# Delete cookie and clean the session
|
||||
response.set_cookie('session.hashes', '', max_age=-1)
|
||||
clean_session(s_id)
|
||||
raise HTTPOKResponse()
|
||||
|
||||
def process(self, _route, arguments={}):
|
||||
"""Process the relevant action for the route
|
||||
|
||||
Call the actions map in order to process the relevant action for
|
||||
the route with the given arguments and process the returned
|
||||
value.
|
||||
|
||||
Keyword arguments:
|
||||
- _route -- The action route as a 2-tuple (method, path)
|
||||
- arguments -- A dict of arguments for the route
|
||||
|
||||
"""
|
||||
try:
|
||||
ret = self.actionsmap.process(arguments, route=_route)
|
||||
except MoulinetteError as e:
|
||||
raise HTTPErrorResponse(e.message)
|
||||
else:
|
||||
return ret
|
||||
|
||||
|
||||
## Signals handlers
|
||||
|
||||
def _do_authenticate(self, authenticator, name, help):
|
||||
"""Process the authentication
|
||||
|
||||
Handle the actionsmap._AMapSignals.authenticate signal.
|
||||
|
||||
"""
|
||||
s_id = request.get_cookie('session.id')
|
||||
try:
|
||||
s_secret = self.secrets[s_id]
|
||||
s_hash = request.get_cookie('session.hashes',
|
||||
secret=s_secret)[name]
|
||||
except KeyError:
|
||||
if name == 'default':
|
||||
msg = _("Needing authentication")
|
||||
else:
|
||||
msg = _("Needing authentication to profile '%s'") % name
|
||||
raise HTTPUnauthorizedResponse(msg)
|
||||
else:
|
||||
return authenticator(token=(s_id, s_hash))
|
||||
|
||||
|
||||
class MoulinetteAPI(object):
|
||||
"""Moulinette Application Programming Interface
|
||||
|
||||
Initialize a HTTP server which serves the API to process moulinette
|
||||
actions.
|
||||
|
||||
Keyword arguments:
|
||||
- actionsmap -- The relevant ActionsMap instance
|
||||
|
@ -166,43 +250,47 @@ class MoulinetteAPI(object):
|
|||
{(method, path): callback}
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, actionsmap, routes={}):
|
||||
# Initialize app and default routes
|
||||
# TODO: Return OK to 'OPTIONS' xhr requests (l173)
|
||||
app = Bottle()
|
||||
app.route(['/api', '/api/<category:re:[a-z]+>'], method='GET',
|
||||
callback=self.doc, skip=['apiauth'])
|
||||
app = Bottle(autojson=False)
|
||||
|
||||
# Append routes from the actions map
|
||||
amap = _ActionsMapPlugin(actionsmap)
|
||||
for (m, p) in actionsmap.parser.routes:
|
||||
app.route(p, method=m, callback=self._error, apply=amap)
|
||||
|
||||
# Append additional routes
|
||||
# TODO: Add an option to skip auth for the route
|
||||
for (m, p), c in routes.items():
|
||||
app.route(p, method=m, callback=c)
|
||||
|
||||
# Define and install a plugin which sets proper header
|
||||
## Wrapper which sets proper header
|
||||
def apiheader(callback):
|
||||
def wrapper(*args, **kwargs):
|
||||
response.content_type = 'application/json'
|
||||
response.set_header('Access-Control-Allow-Origin', '*')
|
||||
return callback(*args, **kwargs)
|
||||
return json_encode(callback(*args, **kwargs))
|
||||
return wrapper
|
||||
|
||||
# Install plugins
|
||||
app.install(apiheader)
|
||||
app.install(_ActionsMapPlugin(actionsmap))
|
||||
|
||||
# Install authentication plugin
|
||||
apiauth = _APIAuthPlugin()
|
||||
app.install(apiauth)
|
||||
# Append default routes
|
||||
app.route(['/api', '/api/<category:re:[a-z]+>'], method='GET',
|
||||
callback=self.doc, skip=['actionsmap'])
|
||||
|
||||
self._app = apiauth.app
|
||||
# Append additional routes
|
||||
# TODO: Add optional authentication to those routes?
|
||||
for (m, p), c in routes.items():
|
||||
app.route(p, method=m, callback=c, skip=['actionsmap'])
|
||||
|
||||
@property
|
||||
def app(self):
|
||||
"""Get Bottle application"""
|
||||
return self._app
|
||||
self._app = app
|
||||
|
||||
def run(self, _port):
|
||||
"""Run the moulinette
|
||||
|
||||
Start a server instance on the given port to serve moulinette
|
||||
actions.
|
||||
|
||||
Keyword arguments:
|
||||
- _port -- Port number to run on
|
||||
|
||||
"""
|
||||
run(self._app, port=_port)
|
||||
|
||||
|
||||
## Routes handlers
|
||||
|
||||
def doc(self, category=None):
|
||||
"""
|
||||
|
@ -220,8 +308,4 @@ class MoulinetteAPI(object):
|
|||
with open('%s/../doc/%s.json' % (pkg.datadir, category)) as f:
|
||||
return f.read()
|
||||
except IOError:
|
||||
return 'unknown'
|
||||
|
||||
def _error(self, *args, **kwargs):
|
||||
# TODO: Raise or return an error
|
||||
print('error')
|
||||
return None
|
||||
|
|
Loading…
Reference in a new issue