mirror of
https://github.com/YunoHost/moulinette.git
synced 2024-09-03 20:06:31 +02:00
One more refactoring in interfaces
* Move actions map parsers classes into their respective interface modules * Introduce an Interface base class for a futur usage * Each interfaces must now implement ActionsMapParser and Interface classes * Standardize interface instantiation
This commit is contained in:
parent
ecd88ce853
commit
b3af4ddaea
9 changed files with 679 additions and 605 deletions
|
@ -45,7 +45,7 @@ if __name__ == '__main__':
|
||||||
api(['yunohost', 'test'], 6787,
|
api(['yunohost', 'test'], 6787,
|
||||||
{('GET', '/installed'): is_installed}, use_cache)
|
{('GET', '/installed'): is_installed}, use_cache)
|
||||||
except MoulinetteError as e:
|
except MoulinetteError as e:
|
||||||
from moulinette.interface.cli import colorize
|
from moulinette.interfaces.cli import colorize
|
||||||
print(_('%s: %s' % (colorize(_('Error'), 'red'), e.strerror)))
|
print(_('%s: %s' % (colorize(_('Error'), 'red'), e.strerror)))
|
||||||
sys.exit(e.code)
|
sys.exit(e.errno)
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
|
@ -26,10 +26,10 @@ __credits__ = """
|
||||||
"""
|
"""
|
||||||
__all__ = [
|
__all__ = [
|
||||||
'init', 'api', 'cli',
|
'init', 'api', 'cli',
|
||||||
'MoulinetteError',
|
'init_interface', 'MoulinetteError',
|
||||||
]
|
]
|
||||||
|
|
||||||
from moulinette.core import MoulinetteError
|
from moulinette.core import init_interface, MoulinetteError
|
||||||
|
|
||||||
|
|
||||||
## Package functions
|
## Package functions
|
||||||
|
@ -76,12 +76,10 @@ def api(namespaces, port, routes={}, use_cache=True):
|
||||||
instead of using the cached one
|
instead of using the cached one
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from moulinette.actionsmap import ActionsMap
|
moulinette = init_interface('api',
|
||||||
from moulinette.interface.api import MoulinetteAPI
|
kwargs={'routes': routes},
|
||||||
|
actionsmap={'namespaces': namespaces,
|
||||||
amap = ActionsMap('api', namespaces, use_cache)
|
'use_cache': use_cache})
|
||||||
moulinette = MoulinetteAPI(amap, routes)
|
|
||||||
|
|
||||||
moulinette.run(port)
|
moulinette.run(port)
|
||||||
|
|
||||||
def cli(namespaces, args, use_cache=True):
|
def cli(namespaces, args, use_cache=True):
|
||||||
|
@ -97,13 +95,12 @@ def cli(namespaces, args, use_cache=True):
|
||||||
instead of using the cached one
|
instead of using the cached one
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from moulinette.actionsmap import ActionsMap
|
from moulinette.interfaces.cli import colorize
|
||||||
from moulinette.interface.cli import MoulinetteCLI, colorize
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
amap = ActionsMap('cli', namespaces, use_cache)
|
moulinette = init_interface('cli',
|
||||||
moulinette = MoulinetteCLI(amap)
|
actionsmap={'namespaces': namespaces,
|
||||||
|
'use_cache': use_cache})
|
||||||
moulinette.run(args)
|
moulinette.run(args)
|
||||||
except MoulinetteError as e:
|
except MoulinetteError as e:
|
||||||
print(_('%s: %s' % (colorize(_('Error'), 'red'), e.strerror)))
|
print(_('%s: %s' % (colorize(_('Error'), 'red'), e.strerror)))
|
||||||
|
|
|
@ -3,19 +3,17 @@
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import errno
|
import errno
|
||||||
|
import logging
|
||||||
import yaml
|
import yaml
|
||||||
import argparse
|
|
||||||
import cPickle as pickle
|
import cPickle as pickle
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
import logging
|
from moulinette.core import (MoulinetteError, MoulinetteLock)
|
||||||
|
from moulinette.interfaces import BaseActionsMapParser
|
||||||
from moulinette.core import (MoulinetteError, MoulinetteLock,
|
|
||||||
init_authenticator)
|
|
||||||
|
|
||||||
## Actions map Signals -------------------------------------------------
|
## Actions map Signals -------------------------------------------------
|
||||||
|
|
||||||
class _AMapSignals(object):
|
class ActionsMapSignals(object):
|
||||||
"""Actions map's Signals interface
|
"""Actions map's Signals interface
|
||||||
|
|
||||||
Allow to easily connect signals of the actions map to handlers. They
|
Allow to easily connect signals of the actions map to handlers. They
|
||||||
|
@ -93,533 +91,7 @@ class _AMapSignals(object):
|
||||||
def _notimplemented(**kwargs):
|
def _notimplemented(**kwargs):
|
||||||
raise NotImplementedError("this signal is not handled")
|
raise NotImplementedError("this signal is not handled")
|
||||||
|
|
||||||
shandler = _AMapSignals()
|
shandler = ActionsMapSignals()
|
||||||
|
|
||||||
|
|
||||||
## Interfaces' Actions map Parser --------------------------------------
|
|
||||||
|
|
||||||
class _AMapParser(object):
|
|
||||||
"""Actions map's base Parser
|
|
||||||
|
|
||||||
Each interfaces must implement a parser class derived from this
|
|
||||||
class. It is used to parse the main parts of the actions map (i.e.
|
|
||||||
global arguments, categories and actions).
|
|
||||||
|
|
||||||
"""
|
|
||||||
def __init__(self, parent=None):
|
|
||||||
if parent:
|
|
||||||
self._o = parent
|
|
||||||
else:
|
|
||||||
self._o = self
|
|
||||||
self._global_conf = {}
|
|
||||||
self._conf = {}
|
|
||||||
|
|
||||||
|
|
||||||
## Virtual properties
|
|
||||||
# Each parser classes must implement these properties.
|
|
||||||
|
|
||||||
"""The name of the interface for which it is the parser"""
|
|
||||||
name = None
|
|
||||||
|
|
||||||
|
|
||||||
## Virtual methods
|
|
||||||
# Each parser classes must implement these methods.
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def format_arg_names(name, full):
|
|
||||||
"""Format argument name
|
|
||||||
|
|
||||||
Format agument name depending on its 'full' parameter and return
|
|
||||||
a list of strings which will be used as name or option strings
|
|
||||||
for the argument parser.
|
|
||||||
|
|
||||||
Keyword arguments:
|
|
||||||
- name -- The argument name
|
|
||||||
- full -- The argument's 'full' parameter
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A list of option strings
|
|
||||||
|
|
||||||
"""
|
|
||||||
raise NotImplementedError("derived class '%s' must override this method" % \
|
|
||||||
self.__class__.__name__)
|
|
||||||
|
|
||||||
def add_global_parser(self, **kwargs):
|
|
||||||
"""Add a parser for global arguments
|
|
||||||
|
|
||||||
Create and return an argument parser for global arguments.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
An ArgumentParser based object
|
|
||||||
|
|
||||||
"""
|
|
||||||
raise NotImplementedError("derived class '%s' must override this method" % \
|
|
||||||
self.__class__.__name__)
|
|
||||||
|
|
||||||
def add_category_parser(self, name, **kwargs):
|
|
||||||
"""Add a parser for a category
|
|
||||||
|
|
||||||
Create a new category and return a parser for it.
|
|
||||||
|
|
||||||
Keyword arguments:
|
|
||||||
- name -- The category name
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A BaseParser based object
|
|
||||||
|
|
||||||
"""
|
|
||||||
raise NotImplementedError("derived class '%s' must override this method" % \
|
|
||||||
self.__class__.__name__)
|
|
||||||
|
|
||||||
def add_action_parser(self, name, tid, **kwargs):
|
|
||||||
"""Add a parser for an action
|
|
||||||
|
|
||||||
Create a new action and return an argument parser for it.
|
|
||||||
|
|
||||||
Keyword arguments:
|
|
||||||
- name -- The action name
|
|
||||||
- tid -- The tuple identifier of the action
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
An ArgumentParser based object
|
|
||||||
|
|
||||||
"""
|
|
||||||
raise NotImplementedError("derived class '%s' must override this method" % \
|
|
||||||
self.__class__.__name__)
|
|
||||||
|
|
||||||
def parse_args(self, args, **kwargs):
|
|
||||||
"""Parse arguments
|
|
||||||
|
|
||||||
Convert argument variables to objects and assign them as
|
|
||||||
attributes of the namespace.
|
|
||||||
|
|
||||||
Keyword arguments:
|
|
||||||
- args -- Arguments string or dict (TODO)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
The populated namespace
|
|
||||||
|
|
||||||
"""
|
|
||||||
raise NotImplementedError("derived class '%s' must override this method" % \
|
|
||||||
self.__class__.__name__)
|
|
||||||
|
|
||||||
|
|
||||||
## Configuration access
|
|
||||||
|
|
||||||
@property
|
|
||||||
def global_conf(self):
|
|
||||||
"""Return the global configuration of the parser"""
|
|
||||||
return self._o._global_conf
|
|
||||||
|
|
||||||
def get_global_conf(self, name, profile='default'):
|
|
||||||
"""Get the global value of a configuration
|
|
||||||
|
|
||||||
Return the formated global value of the configuration 'name' for
|
|
||||||
the given profile. If the configuration doesn't provide profile,
|
|
||||||
the formated default value is returned.
|
|
||||||
|
|
||||||
Keyword arguments:
|
|
||||||
- name -- The configuration name
|
|
||||||
- profile -- The profile of the configuration
|
|
||||||
|
|
||||||
"""
|
|
||||||
if name == 'authenticator':
|
|
||||||
value = self.global_conf[name][profile]
|
|
||||||
else:
|
|
||||||
value = self.global_conf[name]
|
|
||||||
return self._format_conf(name, value)
|
|
||||||
|
|
||||||
def set_global_conf(self, configuration):
|
|
||||||
"""Set global configuration
|
|
||||||
|
|
||||||
Set the global configuration to use for the parser.
|
|
||||||
|
|
||||||
Keyword arguments:
|
|
||||||
- configuration -- The global configuration
|
|
||||||
|
|
||||||
"""
|
|
||||||
self._o._global_conf.update(self._validate_conf(configuration, True))
|
|
||||||
|
|
||||||
def get_conf(self, action, name):
|
|
||||||
"""Get the value of an action configuration
|
|
||||||
|
|
||||||
Return the formated value of configuration 'name' for the action
|
|
||||||
identified by 'action'. If the configuration for the action is
|
|
||||||
not set, the default one is returned.
|
|
||||||
|
|
||||||
Keyword arguments:
|
|
||||||
- action -- An action identifier
|
|
||||||
- name -- The configuration name
|
|
||||||
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
value = self._o._conf[action][name]
|
|
||||||
except KeyError:
|
|
||||||
return self.get_global_conf(name)
|
|
||||||
else:
|
|
||||||
return self._format_conf(name, value)
|
|
||||||
|
|
||||||
def set_conf(self, action, configuration):
|
|
||||||
"""Set configuration for an action
|
|
||||||
|
|
||||||
Set the configuration to use for a given action identified by
|
|
||||||
'action' which is specific to the parser.
|
|
||||||
|
|
||||||
Keyword arguments:
|
|
||||||
- action -- The action identifier
|
|
||||||
- configuration -- The configuration for the action
|
|
||||||
|
|
||||||
"""
|
|
||||||
self._o._conf[action] = self._validate_conf(configuration)
|
|
||||||
|
|
||||||
|
|
||||||
def _validate_conf(self, configuration, is_global=False):
|
|
||||||
"""Validate configuration for the parser
|
|
||||||
|
|
||||||
Return the validated configuration for the interface's actions
|
|
||||||
map parser.
|
|
||||||
|
|
||||||
Keyword arguments:
|
|
||||||
- configuration -- The configuration to pre-format
|
|
||||||
|
|
||||||
"""
|
|
||||||
# TODO: Create a class with a validator method for each configuration
|
|
||||||
conf = {}
|
|
||||||
|
|
||||||
# -- 'authenficate'
|
|
||||||
try:
|
|
||||||
ifaces = configuration['authenticate']
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
if ifaces == 'all':
|
|
||||||
conf['authenticate'] = ifaces
|
|
||||||
elif ifaces == False:
|
|
||||||
conf['authenticate'] = False
|
|
||||||
elif isinstance(ifaces, list):
|
|
||||||
# Store only if authentication is needed
|
|
||||||
conf['authenticate'] = True if self.name in ifaces else False
|
|
||||||
else:
|
|
||||||
# TODO: Log error instead and tell valid values
|
|
||||||
raise MoulinetteError(errno.EINVAL, "Invalid value '%r' for configuration 'authenticate'" % ifaces)
|
|
||||||
|
|
||||||
# -- 'authenticator'
|
|
||||||
try:
|
|
||||||
auth = configuration['authenticator']
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
if not is_global and isinstance(auth, str):
|
|
||||||
try:
|
|
||||||
# Store needed authenticator profile
|
|
||||||
conf['authenticator'] = self.global_conf['authenticator'][auth]
|
|
||||||
except KeyError:
|
|
||||||
raise MoulinetteError(errno.EINVAL, "Undefined authenticator '%s' in global configuration" % auth)
|
|
||||||
elif is_global and isinstance(auth, dict):
|
|
||||||
if len(auth) == 0:
|
|
||||||
logging.warning('no authenticator defined in global configuration')
|
|
||||||
else:
|
|
||||||
auths = {}
|
|
||||||
for auth_name, auth_conf in auth.items():
|
|
||||||
# Add authenticator profile as a 3-tuple
|
|
||||||
# (identifier, configuration, parameters) with
|
|
||||||
# - identifier: the authenticator vendor and its
|
|
||||||
# profile name as a 2-tuple
|
|
||||||
# - configuration: a dict of additional global
|
|
||||||
# configuration (i.e. 'help')
|
|
||||||
# - parameters: a dict of arguments for the
|
|
||||||
# authenticator profile
|
|
||||||
auths[auth_name] = ((auth_conf.get('vendor'), auth_name),
|
|
||||||
{ 'help': auth_conf.get('help', None) },
|
|
||||||
auth_conf.get('parameters', {}))
|
|
||||||
conf['authenticator'] = auths
|
|
||||||
else:
|
|
||||||
# TODO: Log error instead and tell valid values
|
|
||||||
raise MoulinetteError(errno.EINVAL, "Invalid value '%r' for configuration 'authenticator'" % auth)
|
|
||||||
|
|
||||||
# -- 'argument_auth'
|
|
||||||
try:
|
|
||||||
arg_auth = configuration['argument_auth']
|
|
||||||
except KeyError:
|
|
||||||
pass
|
|
||||||
else:
|
|
||||||
if isinstance(arg_auth, bool):
|
|
||||||
conf['argument_auth'] = arg_auth
|
|
||||||
else:
|
|
||||||
# TODO: Log error instead and tell valid values
|
|
||||||
raise MoulinetteError(errno.EINVAL, "Invalid value '%r' for configuration 'argument_auth'" % arg_auth)
|
|
||||||
|
|
||||||
return conf
|
|
||||||
|
|
||||||
def _format_conf(self, name, value):
|
|
||||||
"""Format a configuration value
|
|
||||||
|
|
||||||
Return the formated value of the configuration 'name' from its
|
|
||||||
given value.
|
|
||||||
|
|
||||||
Keyword arguments:
|
|
||||||
- name -- The name of the configuration
|
|
||||||
- value -- The value to format
|
|
||||||
|
|
||||||
"""
|
|
||||||
if name == 'authenticator' and value:
|
|
||||||
(identifier, configuration, parameters) = value
|
|
||||||
|
|
||||||
# Return global configuration and an authenticator
|
|
||||||
# instanciator as a 2-tuple
|
|
||||||
return (configuration,
|
|
||||||
lambda: init_authenticator(identifier, parameters))
|
|
||||||
|
|
||||||
return value
|
|
||||||
|
|
||||||
# CLI Actions map Parser
|
|
||||||
|
|
||||||
class CLIAMapParser(_AMapParser):
|
|
||||||
"""Actions map's CLI Parser
|
|
||||||
|
|
||||||
"""
|
|
||||||
def __init__(self, parent=None, parser=None):
|
|
||||||
super(CLIAMapParser, self).__init__(parent)
|
|
||||||
|
|
||||||
self._parser = parser or argparse.ArgumentParser()
|
|
||||||
self._subparsers = self._parser.add_subparsers()
|
|
||||||
|
|
||||||
|
|
||||||
## Implement virtual properties
|
|
||||||
|
|
||||||
name = 'cli'
|
|
||||||
|
|
||||||
|
|
||||||
## Implement virtual methods
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def format_arg_names(name, full):
|
|
||||||
if name[0] == '-' and full:
|
|
||||||
return [name, full]
|
|
||||||
return [name]
|
|
||||||
|
|
||||||
def add_global_parser(self, **kwargs):
|
|
||||||
return self._parser
|
|
||||||
|
|
||||||
def add_category_parser(self, name, category_help=None, **kwargs):
|
|
||||||
"""Add a parser for a category
|
|
||||||
|
|
||||||
Keyword arguments:
|
|
||||||
- category_help -- A brief description for the category
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A new CLIParser object for the category
|
|
||||||
|
|
||||||
"""
|
|
||||||
parser = self._subparsers.add_parser(name, help=category_help)
|
|
||||||
return self.__class__(self, parser)
|
|
||||||
|
|
||||||
def add_action_parser(self, name, tid, action_help=None, **kwargs):
|
|
||||||
"""Add a parser for an action
|
|
||||||
|
|
||||||
Keyword arguments:
|
|
||||||
- action_help -- A brief description for the action
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A new argparse.ArgumentParser object for the action
|
|
||||||
|
|
||||||
"""
|
|
||||||
return self._subparsers.add_parser(name, help=action_help)
|
|
||||||
|
|
||||||
def parse_args(self, args, **kwargs):
|
|
||||||
ret = self._parser.parse_args(args)
|
|
||||||
|
|
||||||
# Perform authentication if needed
|
|
||||||
if self.get_conf(ret._tid, 'authenticate'):
|
|
||||||
auth_conf, klass = self.get_conf(ret._tid, 'authenticator')
|
|
||||||
|
|
||||||
# TODO: Catch errors
|
|
||||||
auth = shandler.authenticate(klass(), **auth_conf)
|
|
||||||
if not auth.is_authenticated:
|
|
||||||
# TODO: Set proper error code
|
|
||||||
raise MoulinetteError(errno.EACCES, _("This action need authentication"))
|
|
||||||
if self.get_conf(ret._tid, 'argument_auth') and \
|
|
||||||
self.get_conf(ret._tid, 'authenticate') == 'all':
|
|
||||||
ret.auth = auth
|
|
||||||
|
|
||||||
return ret
|
|
||||||
|
|
||||||
# API Actions map Parser
|
|
||||||
|
|
||||||
class _HTTPArgumentParser(object):
|
|
||||||
"""Argument parser for HTTP requests
|
|
||||||
|
|
||||||
Object for parsing HTTP requests into Python objects. It is based
|
|
||||||
on argparse.ArgumentParser class and implements some of its methods.
|
|
||||||
|
|
||||||
"""
|
|
||||||
def __init__(self):
|
|
||||||
# Initialize the ArgumentParser object
|
|
||||||
self._parser = argparse.ArgumentParser(usage='',
|
|
||||||
prefix_chars='@',
|
|
||||||
add_help=False)
|
|
||||||
self._parser.error = self._error
|
|
||||||
|
|
||||||
self._positional = [] # list(arg_name)
|
|
||||||
self._optional = {} # dict({arg_name: option_strings})
|
|
||||||
|
|
||||||
def set_defaults(self, **kwargs):
|
|
||||||
return self._parser.set_defaults(**kwargs)
|
|
||||||
|
|
||||||
def get_default(self, dest):
|
|
||||||
return self._parser.get_default(dest)
|
|
||||||
|
|
||||||
def add_argument(self, *args, **kwargs):
|
|
||||||
action = self._parser.add_argument(*args, **kwargs)
|
|
||||||
|
|
||||||
# Append newly created action
|
|
||||||
if len(action.option_strings) == 0:
|
|
||||||
self._positional.append(action.dest)
|
|
||||||
else:
|
|
||||||
self._optional[action.dest] = action.option_strings
|
|
||||||
|
|
||||||
return action
|
|
||||||
|
|
||||||
def parse_args(self, args={}, namespace=None):
|
|
||||||
arg_strings = []
|
|
||||||
|
|
||||||
## Append an argument to the current one
|
|
||||||
def append(arg_strings, value, option_string=None):
|
|
||||||
# TODO: Process list arguments
|
|
||||||
if isinstance(value, bool):
|
|
||||||
# Append the option string only
|
|
||||||
if option_string is not None:
|
|
||||||
arg_strings.append(option_string)
|
|
||||||
elif isinstance(value, str):
|
|
||||||
if option_string is not None:
|
|
||||||
arg_strings.append(option_string)
|
|
||||||
arg_strings.append(value)
|
|
||||||
else:
|
|
||||||
arg_strings.append(value)
|
|
||||||
|
|
||||||
return arg_strings
|
|
||||||
|
|
||||||
# Iterate over positional arguments
|
|
||||||
for dest in self._positional:
|
|
||||||
if dest in args:
|
|
||||||
arg_strings = append(arg_strings, args[dest])
|
|
||||||
|
|
||||||
# Iterate over optional arguments
|
|
||||||
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, namespace)
|
|
||||||
|
|
||||||
def _error(self, message):
|
|
||||||
# TODO: Raise a proper exception
|
|
||||||
raise MoulinetteError(1, message)
|
|
||||||
|
|
||||||
class APIAMapParser(_AMapParser):
|
|
||||||
"""Actions map's API Parser
|
|
||||||
|
|
||||||
"""
|
|
||||||
def __init__(self):
|
|
||||||
super(APIAMapParser, self).__init__()
|
|
||||||
|
|
||||||
self._parsers = {} # dict({(method, path): _HTTPArgumentParser})
|
|
||||||
|
|
||||||
@property
|
|
||||||
def routes(self):
|
|
||||||
"""Get current routes"""
|
|
||||||
return self._parsers.keys()
|
|
||||||
|
|
||||||
|
|
||||||
## Implement virtual properties
|
|
||||||
|
|
||||||
name = 'api'
|
|
||||||
|
|
||||||
|
|
||||||
## Implement virtual methods
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def format_arg_names(name, full):
|
|
||||||
if name[0] != '-':
|
|
||||||
return [name]
|
|
||||||
if full:
|
|
||||||
return [full.replace('--', '@', 1)]
|
|
||||||
if name.startswith('--'):
|
|
||||||
return [name.replace('--', '@', 1)]
|
|
||||||
return [name.replace('-', '@', 1)]
|
|
||||||
|
|
||||||
def add_global_parser(self, **kwargs):
|
|
||||||
raise AttributeError("global arguments are not managed")
|
|
||||||
|
|
||||||
def add_category_parser(self, name, **kwargs):
|
|
||||||
return self
|
|
||||||
|
|
||||||
def add_action_parser(self, name, tid, api=None, **kwargs):
|
|
||||||
"""Add a parser for an action
|
|
||||||
|
|
||||||
Keyword arguments:
|
|
||||||
- api -- The action route (e.g. 'GET /' )
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
A new _HTTPArgumentParser object for the route
|
|
||||||
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# Validate action route
|
|
||||||
m = re.match('(GET|POST|PUT|DELETE) (/\S+)', api)
|
|
||||||
except TypeError:
|
|
||||||
raise AttributeError("the action '%s' doesn't provide api access" % name)
|
|
||||||
if not m:
|
|
||||||
# TODO: Log error
|
|
||||||
raise ValueError("the action '%s' doesn't provide api access" % name)
|
|
||||||
|
|
||||||
# Check if a parser already exists for the route
|
|
||||||
key = (m.group(1), m.group(2))
|
|
||||||
if key in self.routes:
|
|
||||||
raise AttributeError("a parser for '%s' already exists" % key)
|
|
||||||
|
|
||||||
# Create and append parser
|
|
||||||
parser = _HTTPArgumentParser()
|
|
||||||
self._parsers[key] = (tid, parser)
|
|
||||||
|
|
||||||
# Return the created parser
|
|
||||||
return parser
|
|
||||||
|
|
||||||
def parse_args(self, args, route, **kwargs):
|
|
||||||
"""Parse arguments
|
|
||||||
|
|
||||||
Keyword arguments:
|
|
||||||
- route -- The action route as a 2-tuple (method, path)
|
|
||||||
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# Retrieve the tid and the parser for the route
|
|
||||||
tid, parser = self._parsers[route]
|
|
||||||
except KeyError:
|
|
||||||
raise MoulinetteError(errno.EINVAL, "No parser found for route '%s'" % route)
|
|
||||||
ret = argparse.Namespace()
|
|
||||||
|
|
||||||
# Perform authentication if needed
|
|
||||||
if self.get_conf(tid, 'authenticate'):
|
|
||||||
auth_conf, klass = self.get_conf(tid, 'authenticator')
|
|
||||||
|
|
||||||
# TODO: Catch errors
|
|
||||||
auth = shandler.authenticate(klass(), **auth_conf)
|
|
||||||
if not auth.is_authenticated:
|
|
||||||
# TODO: Set proper error code
|
|
||||||
raise MoulinetteError(errno.EACCES, _("This action need authentication"))
|
|
||||||
if self.get_conf(tid, 'argument_auth') and \
|
|
||||||
self.get_conf(tid, 'authenticate') == 'all':
|
|
||||||
ret.auth = auth
|
|
||||||
|
|
||||||
return parser.parse_args(args, ret)
|
|
||||||
|
|
||||||
"""
|
|
||||||
The dict of interfaces names and their associated parser class.
|
|
||||||
|
|
||||||
"""
|
|
||||||
actionsmap_parsers = {
|
|
||||||
'api': APIAMapParser,
|
|
||||||
'cli': CLIAMapParser
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
## Extra parameters ----------------------------------------------------
|
## Extra parameters ----------------------------------------------------
|
||||||
|
@ -850,35 +322,30 @@ class ExtraArgumentParser(object):
|
||||||
class ActionsMap(object):
|
class ActionsMap(object):
|
||||||
"""Validate and process actions defined into an actions map
|
"""Validate and process actions defined into an actions map
|
||||||
|
|
||||||
The actions map defines the features and their usage of the main
|
The actions map defines the features - and their usage - of an
|
||||||
application. It is composed by categories which contain one or more
|
application which will be available through the moulinette.
|
||||||
action(s). Moreover, the action can have specific argument(s).
|
It is composed by categories which contain one or more action(s).
|
||||||
|
Moreover, the action can have specific argument(s).
|
||||||
|
|
||||||
This class allows to manipulate one or several actions maps
|
This class allows to manipulate one or several actions maps
|
||||||
associated to a namespace. If no namespace is given, it will load
|
associated to a namespace. If no namespace is given, it will load
|
||||||
all available namespaces.
|
all available namespaces.
|
||||||
|
|
||||||
Keyword arguments:
|
Keyword arguments:
|
||||||
- interface -- The type of interface which needs the actions map.
|
- parser -- The BaseActionsMapParser derived class to use for
|
||||||
Possible values are:
|
parsing the actions map
|
||||||
- 'cli' for the command line interface
|
|
||||||
- 'api' for an API usage (HTTP requests)
|
|
||||||
- namespaces -- The list of namespaces to use
|
- namespaces -- The list of namespaces to use
|
||||||
- use_cache -- False if it should parse the actions map file
|
- use_cache -- False if it should parse the actions map file
|
||||||
instead of using the cached one.
|
instead of using the cached one.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def __init__(self, interface, namespaces=[], use_cache=True):
|
def __init__(self, parser, namespaces=[], use_cache=True):
|
||||||
self.use_cache = use_cache
|
self.use_cache = use_cache
|
||||||
self.interface = interface
|
if not issubclass(parser, BaseActionsMapParser):
|
||||||
|
raise MoulinetteError(errno.EINVAL, _("Invalid parser class '%s'" % parser.__name__))
|
||||||
|
self._parser_class = parser
|
||||||
|
|
||||||
try:
|
logging.debug("initializing ActionsMap for the interface '%s'" % parser.interface)
|
||||||
# Retrieve the interface parser
|
|
||||||
self._parser_class = actionsmap_parsers[interface]
|
|
||||||
except KeyError:
|
|
||||||
raise MoulinetteError(errno.EINVAL, _("Unknown interface '%s'" % interface))
|
|
||||||
|
|
||||||
logging.debug("initializing ActionsMap for the '%s' interface" % interface)
|
|
||||||
|
|
||||||
if len(namespaces) == 0:
|
if len(namespaces) == 0:
|
||||||
namespaces = self.get_namespaces()
|
namespaces = self.get_namespaces()
|
||||||
|
@ -903,7 +370,7 @@ class ActionsMap(object):
|
||||||
actionsmaps[n] = yaml.load(f)
|
actionsmaps[n] = yaml.load(f)
|
||||||
|
|
||||||
# Generate parsers
|
# Generate parsers
|
||||||
self.extraparser = ExtraArgumentParser(interface)
|
self.extraparser = ExtraArgumentParser(parser.interface)
|
||||||
self._parser = self._construct_parser(actionsmaps)
|
self._parser = self._construct_parser(actionsmaps)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
@ -1063,7 +530,7 @@ class ActionsMap(object):
|
||||||
parser.set_defaults(_extra=extras)
|
parser.set_defaults(_extra=extras)
|
||||||
|
|
||||||
# Instantiate parser
|
# Instantiate parser
|
||||||
top_parser = self._parser_class()
|
top_parser = self._parser_class(shandler)
|
||||||
|
|
||||||
# Iterate over actions map namespaces
|
# Iterate over actions map namespaces
|
||||||
for n, actionsmap in actionsmaps.items():
|
for n, actionsmap in actionsmaps.items():
|
||||||
|
|
|
@ -12,8 +12,10 @@ class BaseAuthenticator(object):
|
||||||
"""Authenticator base representation
|
"""Authenticator base representation
|
||||||
|
|
||||||
Each authenticators must implement an Authenticator class derived
|
Each authenticators must implement an Authenticator class derived
|
||||||
from this class. It implements base methods to authenticate with a
|
from this class which must overrides virtual properties and methods.
|
||||||
password or a session token.
|
It is used to authenticate and manage session. It implements base
|
||||||
|
methods to authenticate with a password or a session token.
|
||||||
|
|
||||||
Authenticators configurations are identified by a profile name which
|
Authenticators configurations are identified by a profile name which
|
||||||
must be given on instantiation - with the corresponding vendor
|
must be given on instantiation - with the corresponding vendor
|
||||||
configuration of the authenticator.
|
configuration of the authenticator.
|
||||||
|
|
|
@ -131,7 +131,48 @@ class Package(object):
|
||||||
return open('%s/%s' % (self.get_cachedir(**kwargs), filename), mode)
|
return open('%s/%s' % (self.get_cachedir(**kwargs), filename), mode)
|
||||||
|
|
||||||
|
|
||||||
# Authenticators -------------------------------------------------------
|
# Interfaces & Authenticators management -------------------------------
|
||||||
|
|
||||||
|
def init_interface(name, kwargs={}, actionsmap={}):
|
||||||
|
"""Return a new interface instance
|
||||||
|
|
||||||
|
Retrieve the given interface module and return a new instance of its
|
||||||
|
Interface class. It is initialized with arguments 'kwargs' and
|
||||||
|
connected to 'actionsmap' if it's an ActionsMap object, otherwise
|
||||||
|
a new ActionsMap instance will be initialized with arguments
|
||||||
|
'actionsmap'.
|
||||||
|
|
||||||
|
Keyword arguments:
|
||||||
|
- name -- The interface name
|
||||||
|
- kwargs -- A dict of arguments to pass to Interface
|
||||||
|
- actionsmap -- Either an ActionsMap instance or a dict of
|
||||||
|
arguments to pass to ActionsMap
|
||||||
|
|
||||||
|
"""
|
||||||
|
from moulinette.actionsmap import ActionsMap
|
||||||
|
|
||||||
|
try:
|
||||||
|
mod = import_module('moulinette.interfaces.%s' % name)
|
||||||
|
except ImportError:
|
||||||
|
# TODO: List available interfaces
|
||||||
|
raise MoulinetteError(errno.EINVAL, _("Unknown interface '%s'" % name))
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
# Retrieve interface classes
|
||||||
|
parser = mod.ActionsMapParser
|
||||||
|
interface = mod.Interface
|
||||||
|
except AttributeError as e:
|
||||||
|
raise MoulinetteError(errno.EFAULT, _("Invalid interface '%s': %s") % (name, e))
|
||||||
|
|
||||||
|
# Instantiate or retrieve ActionsMap
|
||||||
|
if isinstance(actionsmap, dict):
|
||||||
|
amap = ActionsMap(actionsmap.pop('parser', parser), **actionsmap)
|
||||||
|
elif isinstance(actionsmap, ActionsMap):
|
||||||
|
amap = actionsmap
|
||||||
|
else:
|
||||||
|
raise MoulinetteError(errno.EINVAL, _("Invalid actions map '%r'" % actionsmap))
|
||||||
|
|
||||||
|
return interface(amap, **kwargs)
|
||||||
|
|
||||||
def init_authenticator((vendor, name), kwargs={}):
|
def init_authenticator((vendor, name), kwargs={}):
|
||||||
"""Return a new authenticator instance
|
"""Return a new authenticator instance
|
||||||
|
@ -148,14 +189,24 @@ def init_authenticator((vendor, name), kwargs={}):
|
||||||
try:
|
try:
|
||||||
mod = import_module('moulinette.authenticators.%s' % vendor)
|
mod = import_module('moulinette.authenticators.%s' % vendor)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
# TODO: List available authenticator vendors
|
# TODO: List available authenticators vendors
|
||||||
raise MoulinetteError(errno.EINVAL, _("Unknown authenticator vendor '%s'" % vendor))
|
raise MoulinetteError(errno.EINVAL, _("Unknown authenticator vendor '%s'" % vendor))
|
||||||
else:
|
else:
|
||||||
return mod.Authenticator(name, **kwargs)
|
return mod.Authenticator(name, **kwargs)
|
||||||
|
|
||||||
def clean_session(session_id, profiles=[]):
|
def clean_session(session_id, profiles=[]):
|
||||||
|
"""Clean a session cache
|
||||||
|
|
||||||
|
Remove cache for the session 'session_id' and for profiles in
|
||||||
|
'profiles' or for all of them if the list is empty.
|
||||||
|
|
||||||
|
Keyword arguments:
|
||||||
|
- session_id -- The session id to clean
|
||||||
|
- profiles -- A list of profiles to clean
|
||||||
|
|
||||||
|
"""
|
||||||
sessiondir = pkg.get_cachedir('session')
|
sessiondir = pkg.get_cachedir('session')
|
||||||
if len(profiles) == 0:
|
if not profiles:
|
||||||
profiles = os.listdir(sessiondir)
|
profiles = os.listdir(sessiondir)
|
||||||
|
|
||||||
for p in profiles:
|
for p in profiles:
|
||||||
|
|
307
src/moulinette/interfaces/__init__.py
Normal file
307
src/moulinette/interfaces/__init__.py
Normal file
|
@ -0,0 +1,307 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import errno
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from moulinette.core import (init_authenticator, MoulinetteError)
|
||||||
|
|
||||||
|
# Base Class -----------------------------------------------------------
|
||||||
|
|
||||||
|
class BaseActionsMapParser(object):
|
||||||
|
"""Actions map's base Parser
|
||||||
|
|
||||||
|
Each interfaces must implement an ActionsMapParser class derived
|
||||||
|
from this class which must overrides virtual properties and methods.
|
||||||
|
It is used to parse the main parts of the actions map (i.e. global
|
||||||
|
arguments, categories and actions). It implements methods to set/get
|
||||||
|
the global and actions configuration.
|
||||||
|
|
||||||
|
Keyword arguments:
|
||||||
|
- shandler -- A actionsmap.ActionsMapSignals instance
|
||||||
|
- parent -- A parent BaseActionsMapParser derived object
|
||||||
|
|
||||||
|
"""
|
||||||
|
def __init__(self, shandler, parent=None):
|
||||||
|
if parent:
|
||||||
|
self.shandler = parent.shandler
|
||||||
|
self._o = parent
|
||||||
|
else:
|
||||||
|
self.shandler = shandler
|
||||||
|
self._o = self
|
||||||
|
self._global_conf = {}
|
||||||
|
self._conf = {}
|
||||||
|
|
||||||
|
|
||||||
|
## Virtual properties
|
||||||
|
# Each parser classes must implement these properties.
|
||||||
|
|
||||||
|
"""The name of the interface for which it is the parser"""
|
||||||
|
interface = None
|
||||||
|
|
||||||
|
|
||||||
|
## Virtual methods
|
||||||
|
# Each parser classes must implement these methods.
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def format_arg_names(name, full):
|
||||||
|
"""Format argument name
|
||||||
|
|
||||||
|
Format agument name depending on its 'full' parameter and return
|
||||||
|
a list of strings which will be used as name or option strings
|
||||||
|
for the argument parser.
|
||||||
|
|
||||||
|
Keyword arguments:
|
||||||
|
- name -- The argument name
|
||||||
|
- full -- The argument's 'full' parameter
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A list of option strings
|
||||||
|
|
||||||
|
"""
|
||||||
|
raise NotImplementedError("derived class '%s' must override this method" % \
|
||||||
|
self.__class__.__name__)
|
||||||
|
|
||||||
|
def add_global_parser(self, **kwargs):
|
||||||
|
"""Add a parser for global arguments
|
||||||
|
|
||||||
|
Create and return an argument parser for global arguments.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
An ArgumentParser based object
|
||||||
|
|
||||||
|
"""
|
||||||
|
raise NotImplementedError("derived class '%s' must override this method" % \
|
||||||
|
self.__class__.__name__)
|
||||||
|
|
||||||
|
def add_category_parser(self, name, **kwargs):
|
||||||
|
"""Add a parser for a category
|
||||||
|
|
||||||
|
Create a new category and return a parser for it.
|
||||||
|
|
||||||
|
Keyword arguments:
|
||||||
|
- name -- The category name
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A BaseParser based object
|
||||||
|
|
||||||
|
"""
|
||||||
|
raise NotImplementedError("derived class '%s' must override this method" % \
|
||||||
|
self.__class__.__name__)
|
||||||
|
|
||||||
|
def add_action_parser(self, name, tid, **kwargs):
|
||||||
|
"""Add a parser for an action
|
||||||
|
|
||||||
|
Create a new action and return an argument parser for it.
|
||||||
|
|
||||||
|
Keyword arguments:
|
||||||
|
- name -- The action name
|
||||||
|
- tid -- The tuple identifier of the action
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
An ArgumentParser based object
|
||||||
|
|
||||||
|
"""
|
||||||
|
raise NotImplementedError("derived class '%s' must override this method" % \
|
||||||
|
self.__class__.__name__)
|
||||||
|
|
||||||
|
def parse_args(self, args, **kwargs):
|
||||||
|
"""Parse arguments
|
||||||
|
|
||||||
|
Convert argument variables to objects and assign them as
|
||||||
|
attributes of the namespace.
|
||||||
|
|
||||||
|
Keyword arguments:
|
||||||
|
- args -- Arguments string or dict (TODO)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The populated namespace
|
||||||
|
|
||||||
|
"""
|
||||||
|
raise NotImplementedError("derived class '%s' must override this method" % \
|
||||||
|
self.__class__.__name__)
|
||||||
|
|
||||||
|
|
||||||
|
## Configuration access
|
||||||
|
|
||||||
|
@property
|
||||||
|
def global_conf(self):
|
||||||
|
"""Return the global configuration of the parser"""
|
||||||
|
return self._o._global_conf
|
||||||
|
|
||||||
|
def get_global_conf(self, name, profile='default'):
|
||||||
|
"""Get the global value of a configuration
|
||||||
|
|
||||||
|
Return the formated global value of the configuration 'name' for
|
||||||
|
the given profile. If the configuration doesn't provide profile,
|
||||||
|
the formated default value is returned.
|
||||||
|
|
||||||
|
Keyword arguments:
|
||||||
|
- name -- The configuration name
|
||||||
|
- profile -- The profile of the configuration
|
||||||
|
|
||||||
|
"""
|
||||||
|
if name == 'authenticator':
|
||||||
|
value = self.global_conf[name][profile]
|
||||||
|
else:
|
||||||
|
value = self.global_conf[name]
|
||||||
|
return self._format_conf(name, value)
|
||||||
|
|
||||||
|
def set_global_conf(self, configuration):
|
||||||
|
"""Set global configuration
|
||||||
|
|
||||||
|
Set the global configuration to use for the parser.
|
||||||
|
|
||||||
|
Keyword arguments:
|
||||||
|
- configuration -- The global configuration
|
||||||
|
|
||||||
|
"""
|
||||||
|
self._o._global_conf.update(self._validate_conf(configuration, True))
|
||||||
|
|
||||||
|
def get_conf(self, action, name):
|
||||||
|
"""Get the value of an action configuration
|
||||||
|
|
||||||
|
Return the formated value of configuration 'name' for the action
|
||||||
|
identified by 'action'. If the configuration for the action is
|
||||||
|
not set, the default one is returned.
|
||||||
|
|
||||||
|
Keyword arguments:
|
||||||
|
- action -- An action identifier
|
||||||
|
- name -- The configuration name
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
value = self._o._conf[action][name]
|
||||||
|
except KeyError:
|
||||||
|
return self.get_global_conf(name)
|
||||||
|
else:
|
||||||
|
return self._format_conf(name, value)
|
||||||
|
|
||||||
|
def set_conf(self, action, configuration):
|
||||||
|
"""Set configuration for an action
|
||||||
|
|
||||||
|
Set the configuration to use for a given action identified by
|
||||||
|
'action' which is specific to the parser.
|
||||||
|
|
||||||
|
Keyword arguments:
|
||||||
|
- action -- The action identifier
|
||||||
|
- configuration -- The configuration for the action
|
||||||
|
|
||||||
|
"""
|
||||||
|
self._o._conf[action] = self._validate_conf(configuration)
|
||||||
|
|
||||||
|
|
||||||
|
def _validate_conf(self, configuration, is_global=False):
|
||||||
|
"""Validate configuration for the parser
|
||||||
|
|
||||||
|
Return the validated configuration for the interface's actions
|
||||||
|
map parser.
|
||||||
|
|
||||||
|
Keyword arguments:
|
||||||
|
- configuration -- The configuration to pre-format
|
||||||
|
|
||||||
|
"""
|
||||||
|
# TODO: Create a class with a validator method for each configuration
|
||||||
|
conf = {}
|
||||||
|
|
||||||
|
# -- 'authenficate'
|
||||||
|
try:
|
||||||
|
ifaces = configuration['authenticate']
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if ifaces == 'all':
|
||||||
|
conf['authenticate'] = ifaces
|
||||||
|
elif ifaces == False:
|
||||||
|
conf['authenticate'] = False
|
||||||
|
elif isinstance(ifaces, list):
|
||||||
|
# Store only if authentication is needed
|
||||||
|
conf['authenticate'] = True if self.interface in ifaces else False
|
||||||
|
else:
|
||||||
|
# TODO: Log error instead and tell valid values
|
||||||
|
raise MoulinetteError(errno.EINVAL, "Invalid value '%r' for configuration 'authenticate'" % ifaces)
|
||||||
|
|
||||||
|
# -- 'authenticator'
|
||||||
|
try:
|
||||||
|
auth = configuration['authenticator']
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if not is_global and isinstance(auth, str):
|
||||||
|
try:
|
||||||
|
# Store needed authenticator profile
|
||||||
|
conf['authenticator'] = self.global_conf['authenticator'][auth]
|
||||||
|
except KeyError:
|
||||||
|
raise MoulinetteError(errno.EINVAL, "Undefined authenticator '%s' in global configuration" % auth)
|
||||||
|
elif is_global and isinstance(auth, dict):
|
||||||
|
if len(auth) == 0:
|
||||||
|
logging.warning('no authenticator defined in global configuration')
|
||||||
|
else:
|
||||||
|
auths = {}
|
||||||
|
for auth_name, auth_conf in auth.items():
|
||||||
|
# Add authenticator profile as a 3-tuple
|
||||||
|
# (identifier, configuration, parameters) with
|
||||||
|
# - identifier: the authenticator vendor and its
|
||||||
|
# profile name as a 2-tuple
|
||||||
|
# - configuration: a dict of additional global
|
||||||
|
# configuration (i.e. 'help')
|
||||||
|
# - parameters: a dict of arguments for the
|
||||||
|
# authenticator profile
|
||||||
|
auths[auth_name] = ((auth_conf.get('vendor'), auth_name),
|
||||||
|
{ 'help': auth_conf.get('help', None) },
|
||||||
|
auth_conf.get('parameters', {}))
|
||||||
|
conf['authenticator'] = auths
|
||||||
|
else:
|
||||||
|
# TODO: Log error instead and tell valid values
|
||||||
|
raise MoulinetteError(errno.EINVAL, "Invalid value '%r' for configuration 'authenticator'" % auth)
|
||||||
|
|
||||||
|
# -- 'argument_auth'
|
||||||
|
try:
|
||||||
|
arg_auth = configuration['argument_auth']
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if isinstance(arg_auth, bool):
|
||||||
|
conf['argument_auth'] = arg_auth
|
||||||
|
else:
|
||||||
|
# TODO: Log error instead and tell valid values
|
||||||
|
raise MoulinetteError(errno.EINVAL, "Invalid value '%r' for configuration 'argument_auth'" % arg_auth)
|
||||||
|
|
||||||
|
return conf
|
||||||
|
|
||||||
|
def _format_conf(self, name, value):
|
||||||
|
"""Format a configuration value
|
||||||
|
|
||||||
|
Return the formated value of the configuration 'name' from its
|
||||||
|
given value.
|
||||||
|
|
||||||
|
Keyword arguments:
|
||||||
|
- name -- The name of the configuration
|
||||||
|
- value -- The value to format
|
||||||
|
|
||||||
|
"""
|
||||||
|
if name == 'authenticator' and value:
|
||||||
|
(identifier, configuration, parameters) = value
|
||||||
|
|
||||||
|
# Return global configuration and an authenticator
|
||||||
|
# instanciator as a 2-tuple
|
||||||
|
return (configuration,
|
||||||
|
lambda: init_authenticator(identifier, parameters))
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class BaseInterface(object):
|
||||||
|
"""Moulinette's base Interface
|
||||||
|
|
||||||
|
Each interfaces must implement an Interface class derived from this
|
||||||
|
class which must overrides virtual properties and methods.
|
||||||
|
It is used to provide a user interface for an actions map.
|
||||||
|
|
||||||
|
Keyword arguments:
|
||||||
|
- actionsmap -- The ActionsMap instance to connect to
|
||||||
|
|
||||||
|
"""
|
||||||
|
# TODO: Add common interface methods and try to standardize default ones
|
||||||
|
def __init__(self, actionsmap):
|
||||||
|
raise NotImplementedError("derived class '%s' must override this method" % \
|
||||||
|
self.__class__.__name__)
|
|
@ -1,41 +1,89 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import os
|
||||||
|
import re
|
||||||
import errno
|
import errno
|
||||||
from bottle import run, request, response, Bottle, HTTPResponse
|
import binascii
|
||||||
|
import argparse
|
||||||
from json import dumps as json_encode
|
from json import dumps as json_encode
|
||||||
|
from bottle import run, request, response, Bottle, HTTPResponse
|
||||||
|
|
||||||
from moulinette.core import MoulinetteError, clean_session
|
from moulinette.core import MoulinetteError, clean_session
|
||||||
from moulinette.helpers import YunoHostError, YunoHostLDAP
|
from moulinette.interfaces import (BaseActionsMapParser, BaseInterface)
|
||||||
|
|
||||||
# API helpers ----------------------------------------------------------
|
# API helpers ----------------------------------------------------------
|
||||||
|
|
||||||
import os
|
def random_ascii(length=20):
|
||||||
import binascii
|
"""Return a random ascii string"""
|
||||||
|
return binascii.hexlify(os.urandom(length)).decode('ascii')
|
||||||
|
|
||||||
def random20():
|
class _HTTPArgumentParser(object):
|
||||||
return binascii.hexlify(os.urandom(20)).decode('ascii')
|
"""Argument parser for HTTP requests
|
||||||
|
|
||||||
|
Object for parsing HTTP requests into Python objects. It is based
|
||||||
|
on argparse.ArgumentParser class and implements some of its methods.
|
||||||
|
|
||||||
# HTTP Responses -------------------------------------------------------
|
"""
|
||||||
|
def __init__(self):
|
||||||
|
# Initialize the ArgumentParser object
|
||||||
|
self._parser = argparse.ArgumentParser(usage='',
|
||||||
|
prefix_chars='@',
|
||||||
|
add_help=False)
|
||||||
|
self._parser.error = self._error
|
||||||
|
|
||||||
class HTTPOKResponse(HTTPResponse):
|
self._positional = [] # list(arg_name)
|
||||||
def __init__(self, output=''):
|
self._optional = {} # dict({arg_name: option_strings})
|
||||||
super(HTTPOKResponse, self).__init__(output, 200)
|
|
||||||
|
|
||||||
class HTTPBadRequestResponse(HTTPResponse):
|
def set_defaults(self, **kwargs):
|
||||||
def __init__(self, output=''):
|
return self._parser.set_defaults(**kwargs)
|
||||||
super(HTTPBadRequestResponse, self).__init__(output, 400)
|
|
||||||
|
|
||||||
class HTTPUnauthorizedResponse(HTTPResponse):
|
def get_default(self, dest):
|
||||||
def __init__(self, output=''):
|
return self._parser.get_default(dest)
|
||||||
super(HTTPUnauthorizedResponse, self).__init__(output, 401)
|
|
||||||
|
|
||||||
class HTTPErrorResponse(HTTPResponse):
|
def add_argument(self, *args, **kwargs):
|
||||||
def __init__(self, output=''):
|
action = self._parser.add_argument(*args, **kwargs)
|
||||||
super(HTTPErrorResponse, self).__init__(output, 500)
|
|
||||||
|
|
||||||
|
# Append newly created action
|
||||||
|
if len(action.option_strings) == 0:
|
||||||
|
self._positional.append(action.dest)
|
||||||
|
else:
|
||||||
|
self._optional[action.dest] = action.option_strings
|
||||||
|
|
||||||
# API moulinette interface ---------------------------------------------
|
return action
|
||||||
|
|
||||||
|
def parse_args(self, args={}, namespace=None):
|
||||||
|
arg_strings = []
|
||||||
|
|
||||||
|
## Append an argument to the current one
|
||||||
|
def append(arg_strings, value, option_string=None):
|
||||||
|
# TODO: Process list arguments
|
||||||
|
if isinstance(value, bool):
|
||||||
|
# Append the option string only
|
||||||
|
if option_string is not None:
|
||||||
|
arg_strings.append(option_string)
|
||||||
|
elif isinstance(value, str):
|
||||||
|
if option_string is not None:
|
||||||
|
arg_strings.append(option_string)
|
||||||
|
arg_strings.append(value)
|
||||||
|
else:
|
||||||
|
arg_strings.append(value)
|
||||||
|
|
||||||
|
return arg_strings
|
||||||
|
|
||||||
|
# Iterate over positional arguments
|
||||||
|
for dest in self._positional:
|
||||||
|
if dest in args:
|
||||||
|
arg_strings = append(arg_strings, args[dest])
|
||||||
|
|
||||||
|
# Iterate over optional arguments
|
||||||
|
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, namespace)
|
||||||
|
|
||||||
|
def _error(self, message):
|
||||||
|
# TODO: Raise a proper exception
|
||||||
|
raise MoulinetteError(1, message)
|
||||||
|
|
||||||
class _ActionsMapPlugin(object):
|
class _ActionsMapPlugin(object):
|
||||||
"""Actions map Bottle Plugin
|
"""Actions map Bottle Plugin
|
||||||
|
@ -142,7 +190,7 @@ class _ActionsMapPlugin(object):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# Retrieve session values
|
# Retrieve session values
|
||||||
s_id = request.get_cookie('session.id') or random20()
|
s_id = request.get_cookie('session.id') or random_ascii()
|
||||||
try:
|
try:
|
||||||
s_secret = self.secrets[s_id]
|
s_secret = self.secrets[s_id]
|
||||||
except KeyError:
|
except KeyError:
|
||||||
|
@ -150,7 +198,7 @@ class _ActionsMapPlugin(object):
|
||||||
else:
|
else:
|
||||||
s_hashes = request.get_cookie('session.hashes',
|
s_hashes = request.get_cookie('session.hashes',
|
||||||
secret=s_secret) or {}
|
secret=s_secret) or {}
|
||||||
s_hash = random20()
|
s_hash = random_ascii()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Attempt to authenticate
|
# Attempt to authenticate
|
||||||
|
@ -166,7 +214,7 @@ class _ActionsMapPlugin(object):
|
||||||
else:
|
else:
|
||||||
# Update dicts with new values
|
# Update dicts with new values
|
||||||
s_hashes[profile] = s_hash
|
s_hashes[profile] = s_hash
|
||||||
self.secrets[s_id] = s_secret = random20()
|
self.secrets[s_id] = s_secret = random_ascii()
|
||||||
|
|
||||||
response.set_cookie('session.id', s_id, secure=True)
|
response.set_cookie('session.id', s_id, secure=True)
|
||||||
response.set_cookie('session.hashes', s_hashes, secure=True,
|
response.set_cookie('session.hashes', s_hashes, secure=True,
|
||||||
|
@ -238,14 +286,137 @@ class _ActionsMapPlugin(object):
|
||||||
return authenticator(token=(s_id, s_hash))
|
return authenticator(token=(s_id, s_hash))
|
||||||
|
|
||||||
|
|
||||||
class MoulinetteAPI(object):
|
# HTTP Responses -------------------------------------------------------
|
||||||
"""Moulinette Application Programming Interface
|
|
||||||
|
|
||||||
Initialize a HTTP server which serves the API to process moulinette
|
class HTTPOKResponse(HTTPResponse):
|
||||||
actions.
|
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 Classes Implementation -------------------------------------------
|
||||||
|
|
||||||
|
class ActionsMapParser(BaseActionsMapParser):
|
||||||
|
"""Actions map's Parser for the API
|
||||||
|
|
||||||
|
Provide actions map parsing methods for a CLI usage. The parser for
|
||||||
|
the arguments is represented by a argparse.ArgumentParser object.
|
||||||
|
|
||||||
|
"""
|
||||||
|
def __init__(self, shandler, parent=None):
|
||||||
|
super(ActionsMapParser, self).__init__(shandler, parent)
|
||||||
|
|
||||||
|
self._parsers = {} # dict({(method, path): _HTTPArgumentParser})
|
||||||
|
|
||||||
|
@property
|
||||||
|
def routes(self):
|
||||||
|
"""Get current routes"""
|
||||||
|
return self._parsers.keys()
|
||||||
|
|
||||||
|
|
||||||
|
## Implement virtual properties
|
||||||
|
|
||||||
|
name = 'api'
|
||||||
|
|
||||||
|
|
||||||
|
## Implement virtual methods
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def format_arg_names(name, full):
|
||||||
|
if name[0] != '-':
|
||||||
|
return [name]
|
||||||
|
if full:
|
||||||
|
return [full.replace('--', '@', 1)]
|
||||||
|
if name.startswith('--'):
|
||||||
|
return [name.replace('--', '@', 1)]
|
||||||
|
return [name.replace('-', '@', 1)]
|
||||||
|
|
||||||
|
def add_global_parser(self, **kwargs):
|
||||||
|
raise AttributeError("global arguments are not managed")
|
||||||
|
|
||||||
|
def add_category_parser(self, name, **kwargs):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def add_action_parser(self, name, tid, api=None, **kwargs):
|
||||||
|
"""Add a parser for an action
|
||||||
|
|
||||||
|
Keyword arguments:
|
||||||
|
- api -- The action route (e.g. 'GET /' )
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A new _HTTPArgumentParser object for the route
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Validate action route
|
||||||
|
m = re.match('(GET|POST|PUT|DELETE) (/\S+)', api)
|
||||||
|
except TypeError:
|
||||||
|
raise AttributeError("the action '%s' doesn't provide api access" % name)
|
||||||
|
if not m:
|
||||||
|
# TODO: Log error
|
||||||
|
raise ValueError("the action '%s' doesn't provide api access" % name)
|
||||||
|
|
||||||
|
# Check if a parser already exists for the route
|
||||||
|
key = (m.group(1), m.group(2))
|
||||||
|
if key in self.routes:
|
||||||
|
raise AttributeError("a parser for '%s' already exists" % key)
|
||||||
|
|
||||||
|
# Create and append parser
|
||||||
|
parser = _HTTPArgumentParser()
|
||||||
|
self._parsers[key] = (tid, parser)
|
||||||
|
|
||||||
|
# Return the created parser
|
||||||
|
return parser
|
||||||
|
|
||||||
|
def parse_args(self, args, route, **kwargs):
|
||||||
|
"""Parse arguments
|
||||||
|
|
||||||
|
Keyword arguments:
|
||||||
|
- route -- The action route as a 2-tuple (method, path)
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# Retrieve the tid and the parser for the route
|
||||||
|
tid, parser = self._parsers[route]
|
||||||
|
except KeyError:
|
||||||
|
raise MoulinetteError(errno.EINVAL, "No parser found for route '%s'" % route)
|
||||||
|
ret = argparse.Namespace()
|
||||||
|
|
||||||
|
# Perform authentication if needed
|
||||||
|
if self.get_conf(tid, 'authenticate'):
|
||||||
|
auth_conf, klass = self.get_conf(tid, 'authenticator')
|
||||||
|
|
||||||
|
# TODO: Catch errors
|
||||||
|
auth = self.shandler.authenticate(klass(), **auth_conf)
|
||||||
|
if not auth.is_authenticated:
|
||||||
|
# TODO: Set proper error code
|
||||||
|
raise MoulinetteError(errno.EACCES, _("This action need authentication"))
|
||||||
|
if self.get_conf(tid, 'argument_auth') and \
|
||||||
|
self.get_conf(tid, 'authenticate') == 'all':
|
||||||
|
ret.auth = auth
|
||||||
|
|
||||||
|
return parser.parse_args(args, ret)
|
||||||
|
|
||||||
|
|
||||||
|
class Interface(BaseInterface):
|
||||||
|
"""Application Programming Interface for the moulinette
|
||||||
|
|
||||||
|
Initialize a HTTP server which serves the API connected to a given
|
||||||
|
actions map.
|
||||||
|
|
||||||
Keyword arguments:
|
Keyword arguments:
|
||||||
- actionsmap -- The relevant ActionsMap instance
|
- actionsmap -- The ActionsMap instance to connect to
|
||||||
- routes -- A dict of additional routes to add in the form of
|
- routes -- A dict of additional routes to add in the form of
|
||||||
{(method, path): callback}
|
{(method, path): callback}
|
||||||
|
|
|
@ -2,8 +2,10 @@
|
||||||
|
|
||||||
import errno
|
import errno
|
||||||
import getpass
|
import getpass
|
||||||
|
import argparse
|
||||||
|
|
||||||
from moulinette.core import MoulinetteError
|
from moulinette.core import MoulinetteError
|
||||||
|
from moulinette.interfaces import (BaseActionsMapParser, BaseInterface)
|
||||||
|
|
||||||
# CLI helpers ----------------------------------------------------------
|
# CLI helpers ----------------------------------------------------------
|
||||||
|
|
||||||
|
@ -59,16 +61,93 @@ def pretty_print_dict(d, depth=0):
|
||||||
print((" ") * depth + "%s: %s" % (str(k), v))
|
print((" ") * depth + "%s: %s" % (str(k), v))
|
||||||
|
|
||||||
|
|
||||||
# Moulinette Interface -------------------------------------------------
|
# CLI Classes Implementation -------------------------------------------
|
||||||
|
|
||||||
class MoulinetteCLI(object):
|
class ActionsMapParser(BaseActionsMapParser):
|
||||||
"""Moulinette command-line Interface
|
"""Actions map's Parser for the CLI
|
||||||
|
|
||||||
Initialize an interface connected to the standard input and output
|
Provide actions map parsing methods for a CLI usage. The parser for
|
||||||
stream which allows to process moulinette actions.
|
the arguments is represented by a argparse.ArgumentParser object.
|
||||||
|
|
||||||
Keyword arguments:
|
Keyword arguments:
|
||||||
- actionsmap -- The interface relevant ActionsMap instance
|
- parser -- The argparse.ArgumentParser object to use
|
||||||
|
|
||||||
|
"""
|
||||||
|
def __init__(self, shandler, parent=None, parser=None):
|
||||||
|
super(ActionsMapParser, self).__init__(shandler, parent)
|
||||||
|
|
||||||
|
self._parser = parser or argparse.ArgumentParser()
|
||||||
|
self._subparsers = self._parser.add_subparsers()
|
||||||
|
|
||||||
|
|
||||||
|
## Implement virtual properties
|
||||||
|
|
||||||
|
interface = 'cli'
|
||||||
|
|
||||||
|
|
||||||
|
## Implement virtual methods
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def format_arg_names(name, full):
|
||||||
|
if name[0] == '-' and full:
|
||||||
|
return [name, full]
|
||||||
|
return [name]
|
||||||
|
|
||||||
|
def add_global_parser(self, **kwargs):
|
||||||
|
return self._parser
|
||||||
|
|
||||||
|
def add_category_parser(self, name, category_help=None, **kwargs):
|
||||||
|
"""Add a parser for a category
|
||||||
|
|
||||||
|
Keyword arguments:
|
||||||
|
- category_help -- A brief description for the category
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A new ActionsMapParser object for the category
|
||||||
|
|
||||||
|
"""
|
||||||
|
parser = self._subparsers.add_parser(name, help=category_help)
|
||||||
|
return self.__class__(None, self, parser)
|
||||||
|
|
||||||
|
def add_action_parser(self, name, tid, action_help=None, **kwargs):
|
||||||
|
"""Add a parser for an action
|
||||||
|
|
||||||
|
Keyword arguments:
|
||||||
|
- action_help -- A brief description for the action
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A new argparse.ArgumentParser object for the action
|
||||||
|
|
||||||
|
"""
|
||||||
|
return self._subparsers.add_parser(name, help=action_help)
|
||||||
|
|
||||||
|
def parse_args(self, args, **kwargs):
|
||||||
|
ret = self._parser.parse_args(args)
|
||||||
|
|
||||||
|
# Perform authentication if needed
|
||||||
|
if self.get_conf(ret._tid, 'authenticate'):
|
||||||
|
auth_conf, klass = self.get_conf(ret._tid, 'authenticator')
|
||||||
|
|
||||||
|
# TODO: Catch errors
|
||||||
|
auth = self.shandler.authenticate(klass(), **auth_conf)
|
||||||
|
if not auth.is_authenticated:
|
||||||
|
# TODO: Set proper error code
|
||||||
|
raise MoulinetteError(errno.EACCES, _("This action need authentication"))
|
||||||
|
if self.get_conf(ret._tid, 'argument_auth') and \
|
||||||
|
self.get_conf(ret._tid, 'authenticate') == 'all':
|
||||||
|
ret.auth = auth
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
class Interface(BaseInterface):
|
||||||
|
"""Command-line Interface for the moulinette
|
||||||
|
|
||||||
|
Initialize an interface connected to the standard input/output
|
||||||
|
stream and to a given actions map.
|
||||||
|
|
||||||
|
Keyword arguments:
|
||||||
|
- actionsmap -- The ActionsMap instance to connect to
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def __init__(self, actionsmap):
|
def __init__(self, actionsmap):
|
Loading…
Reference in a new issue