mirror of
https://github.com/YunoHost/moulinette.git
synced 2024-09-03 20:06:31 +02:00
Implement global/actions configuration and MoulinetteCLI
* Modify global configuration in the actions map * Implement getter/setter for global and action configuration * Implement quickly authenticators classes and add a todo LDAP authenticator * Implement an actions map signals system and add some signals * Add a Moulinette Interface for the cli and make it support signals * Add a test namespace which implements configuration and authentication
This commit is contained in:
parent
cdcfa24180
commit
33752ce01b
10 changed files with 655 additions and 48 deletions
|
@ -7,7 +7,7 @@ import os.path
|
||||||
# Run from source
|
# Run from source
|
||||||
basedir = os.path.abspath('%s/../' % os.path.dirname(__file__))
|
basedir = os.path.abspath('%s/../' % os.path.dirname(__file__))
|
||||||
if os.path.isdir('%s/src' % basedir):
|
if os.path.isdir('%s/src' % basedir):
|
||||||
sys.path.append('%s/src' % basedir)
|
sys.path.insert(0, '%s/src' % basedir)
|
||||||
|
|
||||||
from moulinette import init, cli, MoulinetteError
|
from moulinette import init, cli, MoulinetteError
|
||||||
from moulinette.helpers import YunoHostError, colorize
|
from moulinette.helpers import YunoHostError, colorize
|
||||||
|
@ -35,7 +35,7 @@ if __name__ == '__main__':
|
||||||
raise YunoHostError(17, _("YunoHost is not correctly installed, please execute 'yunohost tools postinstall'"))
|
raise YunoHostError(17, _("YunoHost is not correctly installed, please execute 'yunohost tools postinstall'"))
|
||||||
|
|
||||||
# Execute the action
|
# Execute the action
|
||||||
cli(['yunohost'], args, use_cache)
|
cli(['yunohost', 'test'], args, use_cache)
|
||||||
except MoulinetteError as e:
|
except MoulinetteError as e:
|
||||||
print(e.colorize())
|
print(e.colorize())
|
||||||
sys.exit(e.code)
|
sys.exit(e.code)
|
||||||
|
|
48
data/actionsmap/test.yml
Normal file
48
data/actionsmap/test.yml
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
|
||||||
|
#############################
|
||||||
|
# Global parameters #
|
||||||
|
#############################
|
||||||
|
_global:
|
||||||
|
configuration:
|
||||||
|
authenticate:
|
||||||
|
- api
|
||||||
|
authenticator:
|
||||||
|
default:
|
||||||
|
type: ldap
|
||||||
|
help: Admin Password
|
||||||
|
parameters:
|
||||||
|
uri: ldap://localhost:389
|
||||||
|
base: dc=yunohost,dc=org
|
||||||
|
anonymous: false
|
||||||
|
ldap-anonymous:
|
||||||
|
type: ldap
|
||||||
|
parameters:
|
||||||
|
uri: ldap://localhost:389
|
||||||
|
base: dc=yunohost,dc=org
|
||||||
|
anonymous: true
|
||||||
|
argument_auth: true
|
||||||
|
|
||||||
|
#############################
|
||||||
|
# Test Actions #
|
||||||
|
#############################
|
||||||
|
test:
|
||||||
|
actions:
|
||||||
|
non-auth:
|
||||||
|
api: GET /test/non-auth
|
||||||
|
configuration:
|
||||||
|
authenticate: false
|
||||||
|
auth:
|
||||||
|
api: GET /test/auth
|
||||||
|
configuration:
|
||||||
|
authenticate: all
|
||||||
|
auth-cli:
|
||||||
|
api: GET /test/auth-cli
|
||||||
|
configuration:
|
||||||
|
authenticate:
|
||||||
|
- cli
|
||||||
|
anonymous:
|
||||||
|
api: GET /test/anon
|
||||||
|
configuration:
|
||||||
|
authenticate: all
|
||||||
|
authenticator: ldap-anonymous
|
||||||
|
argument_auth: false
|
|
@ -34,8 +34,22 @@
|
||||||
#############################
|
#############################
|
||||||
_global:
|
_global:
|
||||||
configuration:
|
configuration:
|
||||||
auth:
|
authenticate:
|
||||||
- api
|
- api
|
||||||
|
authenticator:
|
||||||
|
default:
|
||||||
|
type: ldap
|
||||||
|
help: Admin Password
|
||||||
|
parameters:
|
||||||
|
uri: ldap://localhost:389
|
||||||
|
base: dc=yunohost,dc=org
|
||||||
|
anonymous: false
|
||||||
|
ldap-anonymous:
|
||||||
|
type: ldap
|
||||||
|
parameters:
|
||||||
|
uri: ldap://localhost:389
|
||||||
|
base: dc=yunohost,dc=org
|
||||||
|
anonymous: true
|
||||||
arguments:
|
arguments:
|
||||||
-v:
|
-v:
|
||||||
full: --version
|
full: --version
|
||||||
|
|
0
lib/test/__init__.py
Executable file
0
lib/test/__init__.py
Executable file
12
lib/test/test.py
Normal file
12
lib/test/test.py
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
|
||||||
|
def test_non_auth():
|
||||||
|
print('non-auth')
|
||||||
|
|
||||||
|
def test_auth(auth):
|
||||||
|
print('[default] / all / auth: %r' % auth)
|
||||||
|
|
||||||
|
def test_auth_cli():
|
||||||
|
print('[default] / cli')
|
||||||
|
|
||||||
|
def test_anonymous():
|
||||||
|
print('[ldap-anonymous] / all')
|
|
@ -57,7 +57,7 @@ def init(**kwargs):
|
||||||
install_i18n()
|
install_i18n()
|
||||||
|
|
||||||
# Add library directory to python path
|
# Add library directory to python path
|
||||||
sys.path.append(pkg.libdir)
|
sys.path.insert(0, pkg.libdir)
|
||||||
|
|
||||||
|
|
||||||
## Easy access to interfaces
|
## Easy access to interfaces
|
||||||
|
@ -99,10 +99,9 @@ def cli(namespaces, args, use_cache=True):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from .actionsmap import ActionsMap
|
from .actionsmap import ActionsMap
|
||||||
from .helpers import pretty_print_dict
|
from .interface.cli import MoulinetteCLI
|
||||||
|
|
||||||
try:
|
amap = ActionsMap('cli', namespaces, use_cache)
|
||||||
amap = ActionsMap('cli', namespaces, use_cache)
|
moulinette = MoulinetteCLI(amap)
|
||||||
pretty_print_dict(amap.process(args))
|
|
||||||
except KeyboardInterrupt, EOFError:
|
moulinette.run(args)
|
||||||
raise MoulinetteError(125, _("Interrupted"))
|
|
||||||
|
|
|
@ -10,7 +10,91 @@ from collections import OrderedDict
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from . import __version__
|
from . import __version__
|
||||||
from .core import MoulinetteError, MoulinetteLock
|
from .core import MoulinetteError, MoulinetteLock, init_authenticator
|
||||||
|
|
||||||
|
## Actions map Signals -------------------------------------------------
|
||||||
|
|
||||||
|
class _AMapSignals(object):
|
||||||
|
"""Actions map's Signals interface
|
||||||
|
|
||||||
|
Allow to easily connect signals of the actions map to handlers. They
|
||||||
|
can be given as arguments in the form of { signal: handler }.
|
||||||
|
|
||||||
|
"""
|
||||||
|
def __init__(self, **kwargs):
|
||||||
|
# Initialize handlers
|
||||||
|
for s in self.signals:
|
||||||
|
self.clear_handler(s)
|
||||||
|
|
||||||
|
# Iterate over signals to connect
|
||||||
|
for s, h in kwargs.items():
|
||||||
|
self.set_handler(s, h)
|
||||||
|
|
||||||
|
def set_handler(self, signal, handler):
|
||||||
|
"""Set the handler for a signal"""
|
||||||
|
if signal not in self.signals:
|
||||||
|
raise ValueError("unknown signal '%s'" % signal)
|
||||||
|
setattr(self, '_%s' % signal, handler)
|
||||||
|
|
||||||
|
def clear_handler(self, signal):
|
||||||
|
"""Clear the handler of a signal"""
|
||||||
|
if signal not in self.signals:
|
||||||
|
raise ValueError("unknown signal '%s'" % signal)
|
||||||
|
setattr(self, '_%s' % signal, self._notimplemented)
|
||||||
|
|
||||||
|
|
||||||
|
## Signals definitions
|
||||||
|
|
||||||
|
"""The list of available signals"""
|
||||||
|
signals = { 'authenticate', 'prompt' }
|
||||||
|
|
||||||
|
def authenticate(self, authenticator, name, help):
|
||||||
|
"""Process the authentication
|
||||||
|
|
||||||
|
Attempt to authenticate to the given authenticator and return
|
||||||
|
it.
|
||||||
|
It is called when authentication is needed (e.g. to process an
|
||||||
|
action).
|
||||||
|
|
||||||
|
Keyword arguments:
|
||||||
|
- authenticator -- The authenticator to use
|
||||||
|
- name -- The authenticator name in the actions map
|
||||||
|
- help -- A help message for the authenticator
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The authenticator object
|
||||||
|
|
||||||
|
"""
|
||||||
|
if authenticator.is_authenticated:
|
||||||
|
return authenticator
|
||||||
|
return self._authenticate(authenticator, name, help)
|
||||||
|
|
||||||
|
def prompt(self, message, is_password=False, confirm=False):
|
||||||
|
"""Prompt for a value
|
||||||
|
|
||||||
|
Prompt the interface for a parameter value which is a password
|
||||||
|
if 'is_password' and must be confirmed if 'confirm'.
|
||||||
|
Is is called when a parameter value is needed and when the
|
||||||
|
current interface should allow user interaction (e.g. to parse
|
||||||
|
extra parameter 'ask' in the cli).
|
||||||
|
|
||||||
|
Keyword arguments:
|
||||||
|
- message -- The message to display
|
||||||
|
- is_password -- True if the parameter is a password
|
||||||
|
- confirm -- True if the value must be confirmed
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The collected value
|
||||||
|
|
||||||
|
"""
|
||||||
|
return self._prompt(message, is_password, confirm)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _notimplemented(**kwargs):
|
||||||
|
raise NotImplementedError("this signal is not handled")
|
||||||
|
|
||||||
|
shandler = _AMapSignals()
|
||||||
|
|
||||||
|
|
||||||
## Interfaces' Actions map Parser --------------------------------------
|
## Interfaces' Actions map Parser --------------------------------------
|
||||||
|
|
||||||
|
@ -22,9 +106,24 @@ class _AMapParser(object):
|
||||||
global arguments, categories and actions).
|
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
|
## Virtual methods
|
||||||
# Each parser classes can implement these methods.
|
# Each parser classes must implement these methods.
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def format_arg_names(name, full):
|
def format_arg_names(name, full):
|
||||||
|
@ -72,13 +171,18 @@ class _AMapParser(object):
|
||||||
raise NotImplementedError("derived class '%s' must override this method" % \
|
raise NotImplementedError("derived class '%s' must override this method" % \
|
||||||
self.__class__.__name__)
|
self.__class__.__name__)
|
||||||
|
|
||||||
def add_action_parser(self, name, **kwargs):
|
def add_action_parser(self, name, tid, conf=None, **kwargs):
|
||||||
"""Add a parser for an action
|
"""Add a parser for an action
|
||||||
|
|
||||||
Create a new action and return an argument parser for it.
|
Create a new action and return an argument parser for it. It
|
||||||
|
should set the configuration 'conf' for the action which can be
|
||||||
|
identified by the tuple identifier 'tid' - it is usually in the
|
||||||
|
form of (namespace, category, action).
|
||||||
|
|
||||||
Keyword arguments:
|
Keyword arguments:
|
||||||
- name -- The action name
|
- name -- The action name
|
||||||
|
- tid -- The tuple identifier of the action
|
||||||
|
- conf -- A dict of configuration for the action
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
An ArgumentParser based object
|
An ArgumentParser based object
|
||||||
|
@ -103,16 +207,194 @@ class _AMapParser(object):
|
||||||
raise NotImplementedError("derived class '%s' must override this method" % \
|
raise NotImplementedError("derived class '%s' must override this method" % \
|
||||||
self.__class__.__name__)
|
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
|
||||||
|
|
||||||
|
"""
|
||||||
|
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):
|
||||||
|
"""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
|
||||||
|
|
||||||
|
"""
|
||||||
|
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(22, "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 parameters of the required authenticator
|
||||||
|
conf['authenticator'] = self.global_conf['authenticator'][auth]
|
||||||
|
except KeyError:
|
||||||
|
raise MoulinetteError(22, "Authenticator '%s' is not defined 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 name
|
||||||
|
auths[auth_name] = ({ 'name': auth_name,
|
||||||
|
'type': auth_conf.get('type'),
|
||||||
|
'help': auth_conf.get('help', None)
|
||||||
|
},
|
||||||
|
auth_conf.get('parameters', {}))
|
||||||
|
conf['authenticator'] = auths
|
||||||
|
else:
|
||||||
|
# TODO: Log error instead and tell valid values
|
||||||
|
raise MoulinetteError(22, "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(22, "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:
|
||||||
|
auth_conf, auth_params = value
|
||||||
|
auth_type = auth_conf.pop('type')
|
||||||
|
|
||||||
|
# Return authenticator configuration and an instanciator for
|
||||||
|
# it as a 2-tuple
|
||||||
|
return (auth_conf,
|
||||||
|
lambda: init_authenticator(auth_type, **auth_params))
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
# CLI Actions map Parser
|
# CLI Actions map Parser
|
||||||
|
|
||||||
class CLIAMapParser(_AMapParser):
|
class CLIAMapParser(_AMapParser):
|
||||||
"""Actions map's CLI Parser
|
"""Actions map's CLI Parser
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def __init__(self, parser=None):
|
def __init__(self, parent=None, parser=None):
|
||||||
|
super(CLIAMapParser, self).__init__(parent)
|
||||||
|
|
||||||
self._parser = parser or argparse.ArgumentParser()
|
self._parser = parser or argparse.ArgumentParser()
|
||||||
self._subparsers = self._parser.add_subparsers()
|
self._subparsers = self._parser.add_subparsers()
|
||||||
|
|
||||||
|
|
||||||
|
## Implement virtual properties
|
||||||
|
|
||||||
|
name = 'cli'
|
||||||
|
|
||||||
|
|
||||||
|
## Implement virtual methods
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def format_arg_names(name, full):
|
def format_arg_names(name, full):
|
||||||
if name[0] == '-' and full:
|
if name[0] == '-' and full:
|
||||||
|
@ -133,9 +415,9 @@ class CLIAMapParser(_AMapParser):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
parser = self._subparsers.add_parser(name, help=category_help)
|
parser = self._subparsers.add_parser(name, help=category_help)
|
||||||
return self.__class__(parser)
|
return self.__class__(self, parser)
|
||||||
|
|
||||||
def add_action_parser(self, name, action_help=None, **kwargs):
|
def add_action_parser(self, name, tid, conf=None, action_help=None, **kwargs):
|
||||||
"""Add a parser for an action
|
"""Add a parser for an action
|
||||||
|
|
||||||
Keyword arguments:
|
Keyword arguments:
|
||||||
|
@ -145,10 +427,27 @@ class CLIAMapParser(_AMapParser):
|
||||||
A new argparse.ArgumentParser object for the action
|
A new argparse.ArgumentParser object for the action
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
if conf:
|
||||||
|
self.set_conf(tid, conf)
|
||||||
return self._subparsers.add_parser(name, help=action_help)
|
return self._subparsers.add_parser(name, help=action_help)
|
||||||
|
|
||||||
def parse_args(self, args, **kwargs):
|
def parse_args(self, args, **kwargs):
|
||||||
return self._parser.parse_args(args)
|
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(1, _("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
|
# API Actions map Parser
|
||||||
|
|
||||||
|
@ -226,13 +525,20 @@ class APIAMapParser(_AMapParser):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self._parsers = {} # dict({(method, path): _HTTPArgumentParser})
|
super(APIAMapParser, self).__init__()
|
||||||
|
|
||||||
|
self._parsers = {} # dict({(method, path): _HTTPArgumentParser})
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def routes(self):
|
def routes(self):
|
||||||
"""Get current routes"""
|
"""Get current routes"""
|
||||||
return self._parsers.keys()
|
return self._parsers.keys()
|
||||||
|
|
||||||
|
|
||||||
|
## Implement virtual properties
|
||||||
|
|
||||||
|
name = 'api'
|
||||||
|
|
||||||
|
|
||||||
## Implement virtual methods
|
## Implement virtual methods
|
||||||
|
|
||||||
|
@ -252,7 +558,7 @@ class APIAMapParser(_AMapParser):
|
||||||
def add_category_parser(self, name, **kwargs):
|
def add_category_parser(self, name, **kwargs):
|
||||||
return self
|
return self
|
||||||
|
|
||||||
def add_action_parser(self, name, api=None, **kwargs):
|
def add_action_parser(self, name, tid, conf=None, api=None, **kwargs):
|
||||||
"""Add a parser for an action
|
"""Add a parser for an action
|
||||||
|
|
||||||
Keyword arguments:
|
Keyword arguments:
|
||||||
|
@ -262,12 +568,13 @@ class APIAMapParser(_AMapParser):
|
||||||
A new _HTTPArgumentParser object for the route
|
A new _HTTPArgumentParser object for the route
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if not api:
|
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)
|
raise AttributeError("the action '%s' doesn't provide api access" % name)
|
||||||
|
|
||||||
# Validate action route
|
|
||||||
m = re.match('(GET|POST|PUT|DELETE) (/\S+)', api)
|
|
||||||
if not m:
|
if not m:
|
||||||
|
# TODO: Log error
|
||||||
raise ValueError("the action '%s' doesn't provide api access" % name)
|
raise ValueError("the action '%s' doesn't provide api access" % name)
|
||||||
|
|
||||||
# Check if a parser already exists for the route
|
# Check if a parser already exists for the route
|
||||||
|
@ -278,6 +585,8 @@ class APIAMapParser(_AMapParser):
|
||||||
# Create and append parser
|
# Create and append parser
|
||||||
parser = _HTTPArgumentParser()
|
parser = _HTTPArgumentParser()
|
||||||
self._parsers[key] = parser
|
self._parsers[key] = parser
|
||||||
|
if conf:
|
||||||
|
self.set_conf(key, conf)
|
||||||
|
|
||||||
# Return the created parser
|
# Return the created parser
|
||||||
return parser
|
return parser
|
||||||
|
@ -293,6 +602,8 @@ class APIAMapParser(_AMapParser):
|
||||||
if route not in self.routes:
|
if route not in self.routes:
|
||||||
raise MoulinetteError(22, "No parser for '%s %s' found" % key)
|
raise MoulinetteError(22, "No parser for '%s %s' found" % key)
|
||||||
|
|
||||||
|
# TODO: Implement authentication
|
||||||
|
|
||||||
return self._parsers[route].parse_args(args)
|
return self._parsers[route].parse_args(args)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
@ -385,9 +696,11 @@ class AskParameter(_ExtraParameter):
|
||||||
if arg_value:
|
if arg_value:
|
||||||
return arg_value
|
return arg_value
|
||||||
|
|
||||||
# Ask for the argument value
|
try:
|
||||||
ret = raw_input(colorize(message + ': ', 'cyan'))
|
# Ask for the argument value
|
||||||
return ret
|
return shandler.prompt(message)
|
||||||
|
except NotImplementedError:
|
||||||
|
return arg_value
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate(klass, value, arg_name):
|
def validate(klass, value, arg_name):
|
||||||
|
@ -415,12 +728,11 @@ class PasswordParameter(AskParameter):
|
||||||
if arg_value:
|
if arg_value:
|
||||||
return arg_value
|
return arg_value
|
||||||
|
|
||||||
# Ask for the password
|
try:
|
||||||
pwd1 = getpass.getpass(colorize(message + ': ', 'cyan'))
|
# Ask for the password
|
||||||
pwd2 = getpass.getpass(colorize('Retype ' + message + ': ', 'cyan'))
|
return shandler.prompt(message, True, True)
|
||||||
if pwd1 != pwd2:
|
except NotImplementedError:
|
||||||
raise MoulinetteError(22, _("Passwords don't match"))
|
return arg_value
|
||||||
return pwd1
|
|
||||||
|
|
||||||
class PatternParameter(_ExtraParameter):
|
class PatternParameter(_ExtraParameter):
|
||||||
"""
|
"""
|
||||||
|
@ -552,6 +864,7 @@ class ActionsMap(object):
|
||||||
"""
|
"""
|
||||||
def __init__(self, interface, namespaces=[], use_cache=True):
|
def __init__(self, interface, namespaces=[], use_cache=True):
|
||||||
self.use_cache = use_cache
|
self.use_cache = use_cache
|
||||||
|
self.interface = interface
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Retrieve the interface parser
|
# Retrieve the interface parser
|
||||||
|
@ -563,7 +876,7 @@ class ActionsMap(object):
|
||||||
|
|
||||||
if len(namespaces) == 0:
|
if len(namespaces) == 0:
|
||||||
namespaces = self.get_namespaces()
|
namespaces = self.get_namespaces()
|
||||||
actionsmaps = {}
|
actionsmaps = OrderedDict()
|
||||||
|
|
||||||
# Iterate over actions map namespaces
|
# Iterate over actions map namespaces
|
||||||
for n in namespaces:
|
for n in namespaces:
|
||||||
|
@ -585,7 +898,26 @@ class ActionsMap(object):
|
||||||
|
|
||||||
# Generate parsers
|
# Generate parsers
|
||||||
self.extraparser = ExtraArgumentParser(interface)
|
self.extraparser = ExtraArgumentParser(interface)
|
||||||
self.parser = self._construct_parser(actionsmaps)
|
self._parser = self._construct_parser(actionsmaps)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def parser(self):
|
||||||
|
"""Return the instance of the interface's actions map parser"""
|
||||||
|
return self._parser
|
||||||
|
|
||||||
|
def connect(self, signal, handler):
|
||||||
|
"""Connect a signal to a handler
|
||||||
|
|
||||||
|
Connect a signal emitted by actions map while processing to a
|
||||||
|
handler. Note that some signals need a return value.
|
||||||
|
|
||||||
|
Keyword arguments:
|
||||||
|
- signal -- The name of the signal
|
||||||
|
- handler -- The method to handle the signal
|
||||||
|
|
||||||
|
"""
|
||||||
|
global shandler
|
||||||
|
shandler.set_handler(signal, handler)
|
||||||
|
|
||||||
def process(self, args, timeout=0, **kwargs):
|
def process(self, args, timeout=0, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
@ -604,7 +936,7 @@ class ActionsMap(object):
|
||||||
arguments[an] = self.extraparser.parse(an, arguments[an], parameters)
|
arguments[an] = self.extraparser.parse(an, arguments[an], parameters)
|
||||||
|
|
||||||
# Retrieve action information
|
# Retrieve action information
|
||||||
namespace, category, action = arguments.pop('_id')
|
namespace, category, action = arguments.pop('_tid')
|
||||||
func_name = '%s_%s' % (category, action.replace('-', '_'))
|
func_name = '%s_%s' % (category, action.replace('-', '_'))
|
||||||
|
|
||||||
# Lock the moulinette for the namespace
|
# Lock the moulinette for the namespace
|
||||||
|
@ -710,7 +1042,9 @@ class ActionsMap(object):
|
||||||
_global = actionsmap.pop('_global', {})
|
_global = actionsmap.pop('_global', {})
|
||||||
|
|
||||||
# -- Parse global configuration
|
# -- Parse global configuration
|
||||||
# TODO
|
if 'configuration' in _global:
|
||||||
|
# Set global configuration
|
||||||
|
top_parser.set_global_conf(_global['configuration'])
|
||||||
|
|
||||||
# -- Parse global arguments
|
# -- Parse global arguments
|
||||||
if 'arguments' in _global:
|
if 'arguments' in _global:
|
||||||
|
@ -737,20 +1071,22 @@ class ActionsMap(object):
|
||||||
|
|
||||||
# -- Parse actions
|
# -- Parse actions
|
||||||
for an, ap in actions.items():
|
for an, ap in actions.items():
|
||||||
arguments = ap.pop('arguments', {})
|
conf = ap.pop('configuration', None)
|
||||||
|
args = ap.pop('arguments', {})
|
||||||
|
tid = (n, cn, an)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Get action parser
|
# Get action parser
|
||||||
parser = cat_parser.add_action_parser(an, **ap)
|
parser = cat_parser.add_action_parser(an, tid, conf, **ap)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
# No parser for the action
|
# No parser for the action
|
||||||
continue
|
continue
|
||||||
except ValueError:
|
except ValueError as e:
|
||||||
# TODO: Log error
|
logging.warning("cannot add action (%s, %s, %s): %s" % (n, cn, an, e))
|
||||||
continue
|
continue
|
||||||
else:
|
else:
|
||||||
# Store action identification and add arguments
|
# Store action identifier and add arguments
|
||||||
parser.set_defaults(_id=(n, cn, an))
|
parser.set_defaults(_tid=tid)
|
||||||
_add_arguments(parser, arguments)
|
_add_arguments(parser, args)
|
||||||
|
|
||||||
return top_parser
|
return top_parser
|
||||||
|
|
|
@ -126,6 +126,80 @@ class Package(object):
|
||||||
return open('%s/%s' % (self.get_cachedir(subdir), filename), mode)
|
return open('%s/%s' % (self.get_cachedir(subdir), filename), mode)
|
||||||
|
|
||||||
|
|
||||||
|
# Authenticators -------------------------------------------------------
|
||||||
|
|
||||||
|
class _BaseAuthenticator(object):
|
||||||
|
|
||||||
|
## Virtual properties
|
||||||
|
# Each authenticator classes must implement these properties.
|
||||||
|
|
||||||
|
"""The name of the authenticator"""
|
||||||
|
name = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_authenticated(self):
|
||||||
|
"""Either the instance is authenticated or not"""
|
||||||
|
raise NotImplementedError("derived class '%s' must override this property" % \
|
||||||
|
self.__class__.__name__)
|
||||||
|
|
||||||
|
|
||||||
|
## Virtual methods
|
||||||
|
# Each authenticator classes must implement these methods.
|
||||||
|
|
||||||
|
def authenticate(password=None, token=None):
|
||||||
|
"""Attempt to authenticate
|
||||||
|
|
||||||
|
Attempt to authenticate with given password or session token.
|
||||||
|
|
||||||
|
Keyword arguments:
|
||||||
|
- password -- A clear text password
|
||||||
|
- token -- A session token
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
An optional session token
|
||||||
|
|
||||||
|
"""
|
||||||
|
raise NotImplementedError("derived class '%s' must override this method" % \
|
||||||
|
self.__class__.__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class LDAPAuthenticator(object):
|
||||||
|
|
||||||
|
def __init__(self, uri, base, anonymous=False):
|
||||||
|
# TODO: Initialize LDAP connection
|
||||||
|
|
||||||
|
if anonymous:
|
||||||
|
self._authenticated = True
|
||||||
|
else:
|
||||||
|
self._authenticated = False
|
||||||
|
|
||||||
|
|
||||||
|
## Implement virtual properties
|
||||||
|
|
||||||
|
name = 'ldap'
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_authenticated(self):
|
||||||
|
return self._authenticated
|
||||||
|
|
||||||
|
|
||||||
|
## Implement virtual methods
|
||||||
|
|
||||||
|
def authenticate(self, password=None, token=None):
|
||||||
|
# TODO: Perform LDAP authentication
|
||||||
|
if password == 'test':
|
||||||
|
self._authenticated = True
|
||||||
|
else:
|
||||||
|
raise MoulinetteError(13, _("Invalid password"))
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
|
||||||
|
def init_authenticator(_name, **kwargs):
|
||||||
|
if _name == 'ldap':
|
||||||
|
return LDAPAuthenticator(**kwargs)
|
||||||
|
|
||||||
|
|
||||||
# Moulinette core classes ----------------------------------------------
|
# Moulinette core classes ----------------------------------------------
|
||||||
|
|
||||||
class MoulinetteError(Exception):
|
class MoulinetteError(Exception):
|
||||||
|
|
|
@ -213,11 +213,11 @@ class MoulinetteAPI(object):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if category is None:
|
if category is None:
|
||||||
with open('%s/doc/resources.json' % pkg.datadir) as f:
|
with open('%s/../doc/resources.json' % pkg.datadir) as f:
|
||||||
return f.read()
|
return f.read()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open('%s/doc/%s.json' % (pkg.datadir, category)) as f:
|
with open('%s/../doc/%s.json' % (pkg.datadir, category)) as f:
|
||||||
return f.read()
|
return f.read()
|
||||||
except IOError:
|
except IOError:
|
||||||
return 'unknown'
|
return 'unknown'
|
||||||
|
|
|
@ -1,5 +1,129 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import getpass
|
||||||
|
|
||||||
|
from ..core import MoulinetteError
|
||||||
|
|
||||||
|
# CLI helpers ----------------------------------------------------------
|
||||||
|
|
||||||
|
colors_codes = {
|
||||||
|
'red' : 31,
|
||||||
|
'green' : 32,
|
||||||
|
'yellow': 33,
|
||||||
|
'cyan' : 34,
|
||||||
|
'purple': 35
|
||||||
|
}
|
||||||
|
|
||||||
|
def colorize(astr, color):
|
||||||
|
"""Colorize a string
|
||||||
|
|
||||||
|
Return a colorized string for printing in shell with style ;)
|
||||||
|
|
||||||
|
Keyword arguments:
|
||||||
|
- astr -- String to colorize
|
||||||
|
- color -- Name of the color
|
||||||
|
|
||||||
|
"""
|
||||||
|
return '\033[{:d}m\033[1m{:s}\033[m'.format(colors_codes[color], astr)
|
||||||
|
|
||||||
|
def pretty_print_dict(d, depth=0):
|
||||||
|
"""Print a dictionary recursively
|
||||||
|
|
||||||
|
Print a dictionary recursively with colors to the standard output.
|
||||||
|
|
||||||
|
Keyword arguments:
|
||||||
|
- d -- The dictionary to print
|
||||||
|
- depth -- The recursive depth of the dictionary
|
||||||
|
|
||||||
|
"""
|
||||||
|
for k,v in sorted(d.items(), key=lambda x: x[0]):
|
||||||
|
k = colorize(str(k), 'purple')
|
||||||
|
if isinstance(v, list) and len(v) == 1:
|
||||||
|
v = v[0]
|
||||||
|
if isinstance(v, dict):
|
||||||
|
print((" ") * depth + ("%s: " % str(k)))
|
||||||
|
pretty_print_dict(v, depth+1)
|
||||||
|
elif isinstance(v, list):
|
||||||
|
print((" ") * depth + ("%s: " % str(k)))
|
||||||
|
for key, value in enumerate(v):
|
||||||
|
if isinstance(value, tuple):
|
||||||
|
pretty_print_dict({value[0]: value[1]}, depth+1)
|
||||||
|
elif isinstance(value, dict):
|
||||||
|
pretty_print_dict({key: value}, depth+1)
|
||||||
|
else:
|
||||||
|
print((" ") * (depth+1) + "- " +str(value))
|
||||||
|
else:
|
||||||
|
if not isinstance(v, basestring):
|
||||||
|
v = str(v)
|
||||||
|
print((" ") * depth + "%s: %s" % (str(k), v))
|
||||||
|
|
||||||
|
|
||||||
|
# Moulinette Interface -------------------------------------------------
|
||||||
|
|
||||||
class MoulinetteCLI(object):
|
class MoulinetteCLI(object):
|
||||||
# TODO: Implement this class
|
"""Moulinette command-line Interface
|
||||||
pass
|
|
||||||
|
Initialize an interface connected to the standard input and output
|
||||||
|
stream which allows to process moulinette action.
|
||||||
|
|
||||||
|
Keyword arguments:
|
||||||
|
- actionsmap -- The interface relevant ActionsMap instance
|
||||||
|
|
||||||
|
"""
|
||||||
|
def __init__(self, actionsmap):
|
||||||
|
# Connect signals to handlers
|
||||||
|
actionsmap.connect('authenticate', self._do_authenticate)
|
||||||
|
actionsmap.connect('prompt', self._do_prompt)
|
||||||
|
|
||||||
|
self.actionsmap = actionsmap
|
||||||
|
|
||||||
|
def run(self, args):
|
||||||
|
"""Run the moulinette
|
||||||
|
|
||||||
|
Process the action corresponding to the given arguments 'args'
|
||||||
|
and print the result.
|
||||||
|
|
||||||
|
Keyword arguments:
|
||||||
|
- args -- A list of argument strings
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
ret = self.actionsmap.process(args, timeout=5)
|
||||||
|
except KeyboardInterrupt, EOFError:
|
||||||
|
raise MoulinetteError(125, _("Interrupted"))
|
||||||
|
|
||||||
|
if isinstance(ret, dict):
|
||||||
|
pretty_print_dict(ret)
|
||||||
|
elif ret:
|
||||||
|
print(ret)
|
||||||
|
|
||||||
|
|
||||||
|
## Signals handlers
|
||||||
|
|
||||||
|
def _do_authenticate(self, authenticator, name, help):
|
||||||
|
"""Process the authentication
|
||||||
|
|
||||||
|
Handle the actionsmap._AMapSignals.authenticate signal.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# TODO: Allow token authentication?
|
||||||
|
msg = help or _("Password")
|
||||||
|
return authenticator.authenticate(password=self._do_prompt(msg, True, False))
|
||||||
|
|
||||||
|
def _do_prompt(self, message, is_password, confirm):
|
||||||
|
"""Prompt for a value
|
||||||
|
|
||||||
|
Handle the actionsmap._AMapSignals.prompt signal.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if is_password:
|
||||||
|
prompt = lambda m: getpass.getpass(colorize(_('%s: ') % m, 'cyan'))
|
||||||
|
else:
|
||||||
|
prompt = lambda m: raw_input(colorize(_('%s: ') % m, 'cyan'))
|
||||||
|
value = prompt(message)
|
||||||
|
|
||||||
|
if confirm:
|
||||||
|
if prompt(_('Retype %s: ') % message) != value:
|
||||||
|
raise MoulinetteError(22, _("Values don't match"))
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
Loading…
Add table
Reference in a new issue