mirror of
https://github.com/YunoHost/moulinette.git
synced 2024-09-03 20:06:31 +02:00
Refactore interfaces and reorganize files a bit
* Remove 'core' folder and reorganize root package structure * Introduce interface's base class and implement 'api' and 'cli' * Add a Package class and a moulinette initialization method * Start to replace YunoHostError by MoulinetteError * Fix actionsmap/yunohost.yml to follow extra parameters rules
This commit is contained in:
parent
2b370be233
commit
9104024fa1
24 changed files with 931 additions and 751 deletions
23
bin/yunohost
23
bin/yunohost
|
@ -5,25 +5,29 @@ import sys
|
|||
import os.path
|
||||
import gettext
|
||||
|
||||
# Debug option
|
||||
if '--debug' in sys.argv:
|
||||
sys.path.append(os.path.abspath(os.path.dirname(__file__) +'/../src'))
|
||||
from moulinette import cli
|
||||
from moulinette.core.helpers import YunoHostError, colorize
|
||||
# Run from source
|
||||
basedir = os.path.abspath(os.path.dirname(__file__) +'/../')
|
||||
if os.path.isdir(basedir +'/src'):
|
||||
sys.path.append(basedir +'/src')
|
||||
|
||||
gettext.install('YunoHost')
|
||||
from moulinette import init, cli, MoulinetteError
|
||||
from moulinette.helpers import YunoHostError, colorize
|
||||
|
||||
gettext.install('yunohost')
|
||||
|
||||
|
||||
## Main action
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Run from source (prefix and libdir set to None)
|
||||
init('yunohost', prefix=None, libdir=None,
|
||||
cachedir=os.path.join(basedir, 'cache'))
|
||||
|
||||
# Additional arguments
|
||||
use_cache = True
|
||||
if '--no-cache' in sys.argv:
|
||||
use_cache = False
|
||||
sys.argv.remove('--no-cache')
|
||||
if '--debug' in sys.argv:
|
||||
sys.argv.remove('--debug')
|
||||
|
||||
try:
|
||||
args = list(sys.argv)
|
||||
|
@ -36,6 +40,9 @@ if __name__ == '__main__':
|
|||
|
||||
# Execute the action
|
||||
cli(args, use_cache)
|
||||
except MoulinetteError as e:
|
||||
print(e.colorize())
|
||||
sys.exit(e.code)
|
||||
except YunoHostError as e:
|
||||
print(colorize(_("Error: "), 'red') + e.message)
|
||||
sys.exit(e.code)
|
||||
|
|
|
@ -5,12 +5,14 @@ import sys
|
|||
import os.path
|
||||
import gettext
|
||||
|
||||
# Debug option
|
||||
if '--debug' in sys.argv:
|
||||
sys.path.append(os.path.abspath(os.path.dirname(__file__) +'/../src'))
|
||||
from moulinette import api
|
||||
# Run from source
|
||||
basedir = os.path.abspath(os.path.dirname(__file__) +'/../')
|
||||
if os.path.isdir(basedir +'/src'):
|
||||
sys.path.append(basedir +'/src')
|
||||
|
||||
gettext.install('YunoHost')
|
||||
from moulinette import init, api
|
||||
|
||||
gettext.install('yunohost')
|
||||
|
||||
|
||||
## Callbacks for additional routes
|
||||
|
@ -29,6 +31,10 @@ def is_installed():
|
|||
## Main action
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Run from source (prefix and libdir set to None)
|
||||
init('yunohost', prefix=None, libdir=None,
|
||||
cachedir=os.path.join(basedir, 'cache'))
|
||||
|
||||
# Additional arguments
|
||||
use_cache = True
|
||||
if '--no-cache' in sys.argv:
|
||||
|
|
|
@ -76,7 +76,9 @@ user:
|
|||
help: Must be unique
|
||||
extra:
|
||||
ask: "Username"
|
||||
pattern: '^[a-z0-9_]+$'
|
||||
pattern:
|
||||
- '^[a-z0-9_]+$'
|
||||
- "Must be alphanumeric and underscore characters only"
|
||||
-f:
|
||||
full: --firstname
|
||||
extra:
|
||||
|
@ -109,7 +111,9 @@ user:
|
|||
nargs: "*"
|
||||
extra:
|
||||
ask: "Users to delete"
|
||||
pattern: '^[a-z0-9_]+$'
|
||||
pattern:
|
||||
- '^[a-z0-9_]+$'
|
||||
- "Must be alphanumeric and underscore characters only"
|
||||
--purge:
|
||||
action: store_true
|
||||
|
||||
|
@ -187,7 +191,9 @@ domain:
|
|||
help: Domain name to add
|
||||
nargs: '+'
|
||||
extra:
|
||||
pattern: '^([a-zA-Z0-9]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)(\.[a-zA-Z0-9]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)*(\.[a-zA-Z]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)$'
|
||||
pattern:
|
||||
- '^([a-zA-Z0-9]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)(\.[a-zA-Z0-9]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)*(\.[a-zA-Z]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)$'
|
||||
- "Must be a valid domain name (e.g. my-domain.org)"
|
||||
-m:
|
||||
full: --main
|
||||
help: Is the main domain
|
||||
|
@ -206,7 +212,9 @@ domain:
|
|||
help: Domain(s) to delete
|
||||
nargs: "+"
|
||||
extra:
|
||||
pattern: '^([a-zA-Z0-9]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)(\.[a-zA-Z0-9]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)*(\.[a-zA-Z]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)$'
|
||||
pattern:
|
||||
- '^([a-zA-Z0-9]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)(\.[a-zA-Z0-9]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)*(\.[a-zA-Z]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)$'
|
||||
- "Must be a valid domain name (e.g. my-domain.org)"
|
||||
|
||||
### domain_info()
|
||||
info:
|
||||
|
@ -216,7 +224,9 @@ domain:
|
|||
domain:
|
||||
help: ""
|
||||
extra:
|
||||
pattern: '^([a-zA-Z0-9]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)(\.[a-zA-Z0-9]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)*(\.[a-zA-Z]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)$'
|
||||
pattern:
|
||||
- '^([a-zA-Z0-9]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)(\.[a-zA-Z0-9]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)*(\.[a-zA-Z]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)$'
|
||||
- "Must be a valid domain name (e.g. my-domain.org)"
|
||||
|
||||
|
||||
#############################
|
||||
|
@ -253,7 +263,9 @@ app:
|
|||
help: Name of the list to remove
|
||||
extra:
|
||||
ask: "List to remove"
|
||||
pattern: '^[a-z0-9_]+$'
|
||||
pattern:
|
||||
- '^[a-z0-9_]+$'
|
||||
- "Must be alphanumeric and underscore characters only"
|
||||
|
||||
### app_list()
|
||||
list:
|
||||
|
@ -302,7 +314,9 @@ app:
|
|||
full: --user
|
||||
help: Allowed app map for a user
|
||||
extra:
|
||||
pattern: '^[a-z0-9_]+$'
|
||||
pattern:
|
||||
- '^[a-z0-9_]+$'
|
||||
- "Must be alphanumeric and underscore characters only"
|
||||
|
||||
|
||||
### app_install() TODO: Write help
|
||||
|
@ -388,7 +402,9 @@ app:
|
|||
port:
|
||||
help: Port to check
|
||||
extra:
|
||||
pattern: '^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$'
|
||||
pattern:
|
||||
- '^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$'
|
||||
- "Must be a valid port number (i.e. 0-65535)"
|
||||
|
||||
### app_checkurl()
|
||||
checkurl:
|
||||
|
@ -659,7 +675,9 @@ service:
|
|||
help: Number of lines to display
|
||||
default: "50"
|
||||
extra:
|
||||
pattern: '^[0-9]+$'
|
||||
pattern:
|
||||
- '^[0-9]+$'
|
||||
- "Must be a valid number"
|
||||
|
||||
|
||||
#############################
|
||||
|
@ -691,7 +709,9 @@ firewall:
|
|||
port:
|
||||
help: Port to open
|
||||
extra:
|
||||
pattern: '^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$'
|
||||
pattern:
|
||||
- '^([0-9]{1,4}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5])$'
|
||||
- "Must be a valid port number (i.e. 0-65535)"
|
||||
protocol:
|
||||
help: Protocol associated with port
|
||||
choices:
|
||||
|
@ -840,12 +860,16 @@ tools:
|
|||
-o:
|
||||
full: --old-domain
|
||||
extra:
|
||||
pattern: '^([a-zA-Z0-9]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)(\.[a-zA-Z0-9]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)*(\.[a-zA-Z]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)$'
|
||||
pattern:
|
||||
- '^([a-zA-Z0-9]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)(\.[a-zA-Z0-9]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)*(\.[a-zA-Z]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)$'
|
||||
- "Must be a valid domain name (e.g. my-domain.org)"
|
||||
-n:
|
||||
full: --new-domain
|
||||
extra:
|
||||
ask: "New main domain"
|
||||
pattern: '^([a-zA-Z0-9]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)(\.[a-zA-Z0-9]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)*(\.[a-zA-Z]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)$'
|
||||
pattern:
|
||||
- '^([a-zA-Z0-9]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)(\.[a-zA-Z0-9]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)*(\.[a-zA-Z]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)$'
|
||||
- "Must be a valid domain name (e.g. my-domain.org)"
|
||||
|
||||
### tools_postinstall()
|
||||
postinstall:
|
||||
|
@ -857,7 +881,9 @@ tools:
|
|||
help: YunoHost main domain
|
||||
extra:
|
||||
ask: "Main domain"
|
||||
pattern: '^([a-zA-Z0-9]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)(\.[a-zA-Z0-9]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)*(\.[a-zA-Z]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)$'
|
||||
pattern:
|
||||
- '^([a-zA-Z0-9]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)(\.[a-zA-Z0-9]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)*(\.[a-zA-Z]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)$'
|
||||
- "Must be a valid domain name (e.g. my-domain.org)"
|
||||
-p:
|
||||
full: --password
|
||||
help: YunoHost admin password
|
||||
|
|
|
@ -24,68 +24,98 @@ __credits__ = """
|
|||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program; if not, see http://www.gnu.org/licenses
|
||||
"""
|
||||
__all__ = [
|
||||
'init', 'api', 'cli',
|
||||
'MoulinetteError',
|
||||
]
|
||||
|
||||
from .core import MoulinetteError
|
||||
|
||||
curr_namespace = None
|
||||
|
||||
|
||||
## Fast access functions
|
||||
## Package functions
|
||||
|
||||
def init(namespace=None, **kwargs):
|
||||
"""Package initialization
|
||||
|
||||
Initialize directories and global variables. It must be called
|
||||
before any of package method is used - even the easy access
|
||||
functions.
|
||||
|
||||
Keyword arguments:
|
||||
- namespace -- The namespace to initialize and use
|
||||
- **kwargs -- See helpers.Package
|
||||
|
||||
At the end, the global variable 'pkg' will contain a Package
|
||||
instance. See helpers.Package for available methods and variables.
|
||||
|
||||
"""
|
||||
import __builtin__
|
||||
from .core import Package
|
||||
|
||||
global curr_namespace
|
||||
curr_namespace = namespace
|
||||
|
||||
__builtin__.__dict__['pkg'] = Package(**kwargs)
|
||||
|
||||
|
||||
## Easy access to interfaces
|
||||
|
||||
def api(port, routes={}, use_cache=True):
|
||||
"""
|
||||
"""Web server (API) interface
|
||||
|
||||
Run a HTTP server with the moulinette for an API usage.
|
||||
|
||||
Keyword arguments:
|
||||
|
||||
- port -- Port to run on
|
||||
|
||||
- routes -- A dict of additional routes to add in the form of
|
||||
{(method, uri): callback}
|
||||
|
||||
- use_cache -- False if it should parse the actions map file
|
||||
instead of using the cached one
|
||||
|
||||
"""
|
||||
from bottle import run
|
||||
from core.actionsmap import ActionsMap
|
||||
from core.api import MoulinetteAPI
|
||||
from core.helpers import Interface
|
||||
from .actionsmap import ActionsMap
|
||||
from .interface.api import MoulinetteAPI
|
||||
|
||||
amap = ActionsMap(Interface.api, use_cache=use_cache)
|
||||
amap = ActionsMap('api', use_cache=use_cache)
|
||||
moulinette = MoulinetteAPI(amap, routes)
|
||||
|
||||
run(moulinette.app, port=port)
|
||||
|
||||
def cli(args, use_cache=True):
|
||||
"""
|
||||
"""Command line interface
|
||||
|
||||
Execute an action with the moulinette from the CLI and print its
|
||||
result in a readable format.
|
||||
|
||||
Keyword arguments:
|
||||
|
||||
- args -- A list of argument strings
|
||||
|
||||
- use_cache -- False if it should parse the actions map file
|
||||
instead of using the cached one
|
||||
|
||||
"""
|
||||
import os
|
||||
from core.actionsmap import ActionsMap
|
||||
from core.helpers import Interface, YunoHostError, pretty_print_dict
|
||||
from .actionsmap import ActionsMap
|
||||
from .helpers import YunoHostError, pretty_print_dict
|
||||
|
||||
lock_file = '/var/run/moulinette.lock'
|
||||
|
||||
# TODO: Move the lock checking into the ActionsMap class
|
||||
# Check the lock
|
||||
if os.path.isfile(lock_file):
|
||||
raise YunoHostError(1, _("The moulinette is already running"))
|
||||
raise MoulinetteError(1, _("The moulinette is already running"))
|
||||
|
||||
# Create a lock
|
||||
with open(lock_file, 'w') as f: pass
|
||||
os.system('chmod 400 '+ lock_file)
|
||||
|
||||
try:
|
||||
amap = ActionsMap(Interface.cli, use_cache=use_cache)
|
||||
amap = ActionsMap('cli', use_cache=use_cache)
|
||||
pretty_print_dict(amap.process(args))
|
||||
except KeyboardInterrupt, EOFError:
|
||||
raise YunoHostError(125, _("Interrupted"))
|
||||
raise MoulinetteError(125, _("Interrupted"))
|
||||
finally:
|
||||
# Remove the lock
|
||||
os.remove(lock_file)
|
||||
|
|
338
src/moulinette/actionsmap.py
Normal file
338
src/moulinette/actionsmap.py
Normal file
|
@ -0,0 +1,338 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import pickle
|
||||
import yaml
|
||||
import re
|
||||
import os
|
||||
from collections import OrderedDict
|
||||
|
||||
import logging
|
||||
|
||||
from . import __version__, curr_namespace, MoulinetteError
|
||||
from .extra.parameters import extraparameters_list
|
||||
|
||||
## Extra parameters Parser
|
||||
|
||||
class ExtraParser(object):
|
||||
"""
|
||||
Global parser for the extra parameters.
|
||||
|
||||
"""
|
||||
def __init__(self, iface):
|
||||
self.iface = iface
|
||||
self.extra = OrderedDict()
|
||||
|
||||
# Append available extra parameters for the current interface
|
||||
for klass in extraparameters_list:
|
||||
if iface in klass.skipped_iface:
|
||||
continue
|
||||
self.extra[klass.name] = klass
|
||||
|
||||
def validate(self, arg_name, parameters):
|
||||
"""
|
||||
Validate values of extra parameters for an argument
|
||||
|
||||
Keyword arguments:
|
||||
- arg_name -- The argument name
|
||||
- parameters -- A dict of extra parameters with their values
|
||||
|
||||
"""
|
||||
# Iterate over parameters to validate
|
||||
for p, v in parameters.items():
|
||||
# Remove unknow parameters
|
||||
if p not in self.extra.keys():
|
||||
del parameters[p]
|
||||
|
||||
# Validate parameter value
|
||||
parameters[p] = self.extra[p].validate(v, arg_name)
|
||||
|
||||
return parameters
|
||||
|
||||
def parse(self, arg_name, arg_value, parameters):
|
||||
"""
|
||||
Parse argument with extra parameters
|
||||
|
||||
Keyword arguments:
|
||||
- arg_name -- The argument name
|
||||
- arg_value -- The argument value
|
||||
- parameters -- A dict of extra parameters with their values
|
||||
|
||||
"""
|
||||
# Iterate over available parameters
|
||||
for p, klass in self.extra.items():
|
||||
if p not in parameters.keys():
|
||||
continue
|
||||
|
||||
# Initialize the extra parser
|
||||
parser = klass(self.iface)
|
||||
|
||||
# Parse the argument
|
||||
if isinstance(arg_value, list):
|
||||
for v in arg_value:
|
||||
r = parser(parameters[p], arg_name, v)
|
||||
if r not in arg_value:
|
||||
arg_value.append(r)
|
||||
else:
|
||||
arg_value = parser(parameters[p], arg_name, arg_value)
|
||||
|
||||
return arg_value
|
||||
|
||||
|
||||
## Main class
|
||||
|
||||
class ActionsMap(object):
|
||||
"""
|
||||
Validate and process action defined into the actions map.
|
||||
|
||||
The actions map defines features and their usage of the main
|
||||
application. It is composed by categories which contain one or more
|
||||
action(s). Moreover, the action can have specific argument(s).
|
||||
|
||||
Keyword arguments:
|
||||
|
||||
- interface -- Interface type that requires the actions map.
|
||||
Possible value is one of:
|
||||
- 'cli' for the command line interface
|
||||
- 'api' for an API usage (HTTP requests)
|
||||
|
||||
- use_cache -- False if it should parse the actions map file
|
||||
instead of using the cached one.
|
||||
|
||||
"""
|
||||
def __init__(self, interface, use_cache=True):
|
||||
self.use_cache = use_cache
|
||||
|
||||
try:
|
||||
# Retrieve the interface parser
|
||||
mod = __import__('interface.%s' % interface,
|
||||
globals=globals(), level=1,
|
||||
fromlist=['actionsmap_parser'])
|
||||
parser = getattr(mod, 'actionsmap_parser')
|
||||
except (AttributeError, ImportError):
|
||||
raise MoulinetteError(22, _("Invalid interface '%s'" % interface))
|
||||
else:
|
||||
self._parser_class = parser
|
||||
|
||||
logging.debug("initializing ActionsMap for the '%s' interface" % interface)
|
||||
|
||||
actionsmaps = {}
|
||||
namespaces = self.get_actionsmap_namespaces()
|
||||
if curr_namespace and curr_namespace in namespaces:
|
||||
namespaces = [curr_namespace]
|
||||
|
||||
# Iterate over actions map namespaces
|
||||
for n in namespaces:
|
||||
logging.debug("loading '%s' actions map namespace" % n)
|
||||
|
||||
if use_cache:
|
||||
# Attempt to load cache if it exists
|
||||
cache_file = '%s/%s.pkl' % (pkg.cachedir('actionsmap'), n)
|
||||
if os.path.isfile(cache_file):
|
||||
with open(cache_file, 'r') as f:
|
||||
actionsmaps[n] = pickle.load(f)
|
||||
else:
|
||||
self.use_cache = False
|
||||
actionsmaps = self.generate_cache(namespaces)
|
||||
break
|
||||
else:
|
||||
am_file = '%s/%s.yml' % (pkg.datadir('actionsmap'), n)
|
||||
with open(am_file, 'r') as f:
|
||||
actionsmaps[n] = yaml.load(f)
|
||||
|
||||
# Generate parsers
|
||||
self.extraparser = ExtraParser(interface)
|
||||
self.parser = self._construct_parser(actionsmaps)
|
||||
|
||||
def process(self, args, **kwargs):
|
||||
"""
|
||||
Parse arguments and process the proper action
|
||||
|
||||
Keyword arguments:
|
||||
- args -- The arguments to parse
|
||||
- **kwargs -- Additional interface arguments
|
||||
|
||||
"""
|
||||
# Parse arguments
|
||||
arguments = vars(self.parser.parse_args(args, **kwargs))
|
||||
arguments = self._parse_extra_parameters(arguments)
|
||||
|
||||
# Retrieve action information
|
||||
namespace, category, action = arguments.pop('_info')
|
||||
func_name = '%s_%s' % (category, action)
|
||||
|
||||
try:
|
||||
mod = __import__('%s.%s' % (namespace, category),
|
||||
globals=globals(), level=0,
|
||||
fromlist=[func_name])
|
||||
func = getattr(mod, func_name)
|
||||
except (AttributeError, ImportError):
|
||||
raise MoulinetteError(168, _('Function is not defined'))
|
||||
else:
|
||||
# Process the action
|
||||
return func(**arguments)
|
||||
|
||||
@staticmethod
|
||||
def get_actionsmap_namespaces():
|
||||
"""
|
||||
Retrieve actions map namespaces from a given path
|
||||
|
||||
Returns:
|
||||
A list of available namespaces
|
||||
|
||||
"""
|
||||
namespaces = []
|
||||
|
||||
for f in os.listdir(pkg.datadir('actionsmap')):
|
||||
if f.endswith('.yml'):
|
||||
namespaces.append(f[:-4])
|
||||
return namespaces
|
||||
|
||||
@classmethod
|
||||
def generate_cache(klass, namespaces=None):
|
||||
"""
|
||||
Generate cache for the actions map's file(s)
|
||||
|
||||
Keyword arguments:
|
||||
- namespaces -- A list of namespaces to generate cache for
|
||||
|
||||
Returns:
|
||||
A dict of actions map for each namespaces
|
||||
|
||||
"""
|
||||
actionsmaps = {}
|
||||
if not namespaces:
|
||||
namespaces = klass.get_actionsmap_namespaces()
|
||||
|
||||
# Iterate over actions map namespaces
|
||||
for n in namespaces:
|
||||
logging.debug("generating cache for '%s' actions map namespace" % n)
|
||||
|
||||
# Read actions map from yaml file
|
||||
am_file = pkg.datafile('actionsmap/%s.yml' % n)
|
||||
with open(am_file, 'r') as f:
|
||||
actionsmaps[n] = yaml.load(f)
|
||||
|
||||
# Cache actions map into pickle file
|
||||
cache_file = pkg.cachefile('actionsmap/%s.pkl' % n, make_dir=True)
|
||||
with open(cache_file, 'w') as f:
|
||||
pickle.dump(actionsmaps[n], f)
|
||||
|
||||
return actionsmaps
|
||||
|
||||
|
||||
## Private class and methods
|
||||
|
||||
def _store_extra_parameters(self, parser, arg_name, arg_extra):
|
||||
"""
|
||||
Store extra parameters for a given argument
|
||||
|
||||
Keyword arguments:
|
||||
- parser -- Parser object for the arguments
|
||||
- arg_name -- Argument name
|
||||
- arg_extra -- Argument extra parameters
|
||||
|
||||
Returns:
|
||||
The parser object
|
||||
|
||||
"""
|
||||
if arg_extra:
|
||||
# Retrieve current extra parameters dict
|
||||
extra = parser.get_default('_extra')
|
||||
if not extra or not isinstance(extra, dict):
|
||||
extra = {}
|
||||
|
||||
if not self.use_cache:
|
||||
# Validate extra parameters for the argument
|
||||
extra[arg_name] = self.extraparser.validate(arg_name, arg_extra)
|
||||
else:
|
||||
extra[arg_name] = arg_extra
|
||||
parser.set_defaults(_extra=extra)
|
||||
|
||||
return parser
|
||||
|
||||
def _parse_extra_parameters(self, args):
|
||||
"""
|
||||
Parse arguments with their extra parameters
|
||||
|
||||
Keyword arguments:
|
||||
- args -- A dict of all arguments
|
||||
|
||||
Return:
|
||||
The parsed arguments dict
|
||||
|
||||
"""
|
||||
# Retrieve extra parameters for the arguments
|
||||
extra = args.pop('_extra', None)
|
||||
if not extra:
|
||||
return args
|
||||
|
||||
# Validate extra parameters for each arguments
|
||||
for an, parameters in extra.items():
|
||||
args[an] = self.extraparser.parse(an, args[an], parameters)
|
||||
|
||||
return args
|
||||
|
||||
def _construct_parser(self, actionsmaps):
|
||||
"""
|
||||
Construct the parser with the actions map
|
||||
|
||||
Keyword arguments:
|
||||
- actionsmaps -- A dict of multi-level dictionnary of
|
||||
categories/actions/arguments list for each namespaces
|
||||
|
||||
Returns:
|
||||
An interface relevant's parser object
|
||||
|
||||
"""
|
||||
# Instantiate parser
|
||||
top_parser = self._parser_class()
|
||||
|
||||
# Iterate over actions map namespaces
|
||||
for n, actionsmap in actionsmaps.items():
|
||||
if 'general_arguments' in actionsmap:
|
||||
# Parse general arguments
|
||||
if top_parser.parse_general:
|
||||
parser = top_parser.add_general_parser()
|
||||
for an, ap in actionsmap['general_arguments'].items():
|
||||
if 'version' in ap:
|
||||
ap['version'] = ap['version'].replace('%version%',
|
||||
__version__)
|
||||
argname = top_parser.format_arg_name(an, ap.pop('full', None))
|
||||
parser.add_argument(*argname, **ap)
|
||||
del actionsmap['general_arguments']
|
||||
|
||||
# Parse categories
|
||||
for cn, cp in actionsmap.items():
|
||||
if 'actions' not in cp:
|
||||
continue
|
||||
actions = cp.pop('actions')
|
||||
|
||||
# Add category parser
|
||||
if top_parser.parse_category:
|
||||
cat_parser = top_parser.add_category_parser(cn, **cp)
|
||||
else:
|
||||
cat_parser = top_parser
|
||||
|
||||
# Parse actions
|
||||
if not top_parser.parse_action:
|
||||
continue
|
||||
for an, ap in actions.items():
|
||||
arguments = ap.pop('arguments', {})
|
||||
|
||||
# Add action parser
|
||||
parser = cat_parser.add_action_parser(an, **ap)
|
||||
if not parser:
|
||||
continue
|
||||
|
||||
# Store action information
|
||||
parser.set_defaults(_info=(n, cn, an))
|
||||
|
||||
# Add action arguments
|
||||
for argn, argp in arguments.items():
|
||||
name = top_parser.format_arg_name(argn, argp.pop('full', None))
|
||||
extra = argp.pop('extra', None)
|
||||
|
||||
arg = parser.add_argument(*name, **argp)
|
||||
parser = self._store_extra_parameters(parser, arg.dest, extra)
|
||||
|
||||
return top_parser
|
|
@ -1,16 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
# TODO: Remove permanent debug values
|
||||
import os
|
||||
|
||||
# Path for the the web sessions
|
||||
session_path = '/var/cache/yunohost/session'
|
||||
|
||||
# Path of the actions map definition(s)
|
||||
actionsmap_path = os.path.dirname(__file__) +'/../../data/actionsmap'
|
||||
|
||||
# Path for the actions map cache
|
||||
actionsmap_cache_path = '/var/cache/yunohost/actionsmap'
|
||||
|
||||
# Path of the doc in json format
|
||||
doc_json_path = os.path.dirname(__file__) +'/../../doc'
|
155
src/moulinette/core.py
Normal file
155
src/moulinette/core.py
Normal file
|
@ -0,0 +1,155 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import os
|
||||
import sys
|
||||
import gettext
|
||||
from .helpers import colorize
|
||||
|
||||
class Package(object):
|
||||
"""Package representation and easy access
|
||||
|
||||
Initialize directories and variables for the package and give them
|
||||
easy access.
|
||||
|
||||
Keyword arguments:
|
||||
- prefix -- The installation prefix
|
||||
- libdir -- The library directory; usually, this would be
|
||||
prefix + '/lib' (or '/lib64') when installed
|
||||
- cachedir -- The cache directory; usually, this would be
|
||||
'/var/cache' when installed
|
||||
- destdir -- The destination prefix only if it's an installation
|
||||
|
||||
'prefix' and 'libdir' arguments should be empty in order to run
|
||||
package from source.
|
||||
|
||||
"""
|
||||
def __init__(self, prefix, libdir, cachedir, destdir=None):
|
||||
if not prefix and not libdir:
|
||||
# Running from source directory
|
||||
basedir = os.path.abspath(os.path.dirname(sys.argv[0]) +'/../')
|
||||
self._datadir = os.path.join(basedir, 'data')
|
||||
self._libdir = os.path.join(basedir, 'src')
|
||||
self._cachedir = cachedir
|
||||
else:
|
||||
self._datadir = os.path.join(prefix, 'share/moulinette')
|
||||
self._libdir = os.path.join(libdir, 'moulinette')
|
||||
self._cachedir = os.path.join(cachedir, 'moulinette')
|
||||
|
||||
# Append library path to python's path
|
||||
sys.path.append(self._libdir)
|
||||
self._destdir = destdir or None
|
||||
|
||||
|
||||
## Easy access to directories and files
|
||||
|
||||
def datadir(self, subdir=None, **kwargs):
|
||||
"""Return the path to a data directory"""
|
||||
return self.get_dir(self._datadir, subdir, **kwargs)
|
||||
|
||||
def datafile(self, filename, **kwargs):
|
||||
"""Return the path to a data file"""
|
||||
return self.get_file(self._datadir, filename, **kwargs)
|
||||
|
||||
def libdir(self, subdir=None, **kwargs):
|
||||
"""Return the path to a lib directory"""
|
||||
return self.get_dir(self._libdir, subdir, **kwargs)
|
||||
|
||||
def libfile(self, filename, **kwargs):
|
||||
"""Return the path to a lib file"""
|
||||
return self.get_file(self._libdir, filename, **kwargs)
|
||||
|
||||
def cachedir(self, subdir=None, **kwargs):
|
||||
"""Return the path to a cache directory"""
|
||||
return self.get_dir(self._cachedir, subdir, **kwargs)
|
||||
|
||||
def cachefile(self, filename, **kwargs):
|
||||
"""Return the path to a cache file"""
|
||||
return self.get_file(self._cachedir, filename, **kwargs)
|
||||
|
||||
|
||||
## Standard methods
|
||||
|
||||
def get_dir(self, basedir, subdir=None, make_dir=False):
|
||||
"""Get a directory path
|
||||
|
||||
Return a path composed by a base directory and an optional
|
||||
subdirectory. The path will be created if needed.
|
||||
|
||||
Keyword arguments:
|
||||
- basedir -- The base directory
|
||||
- subdir -- An optional subdirectory
|
||||
- make_dir -- True if it should create needed directory
|
||||
|
||||
"""
|
||||
# Retrieve path
|
||||
path = basedir
|
||||
if self._destdir:
|
||||
path = os.path.join(self._destdir, path)
|
||||
if subdir:
|
||||
path = os.path.join(path, subdir)
|
||||
|
||||
# Create directory
|
||||
if make_dir and not os.path.isdir(path):
|
||||
os.makedirs(path)
|
||||
return path
|
||||
|
||||
def get_file(self, basedir, filename, **kwargs):
|
||||
"""Get a file path
|
||||
|
||||
Return the path of the filename in the specified directory. This
|
||||
directory will be created if needed.
|
||||
|
||||
Keyword arguments:
|
||||
- basedir -- The base directory of the file
|
||||
- filename -- The filename or a path relative to basedir
|
||||
- **kwargs -- Additional arguments for Package.get_dir
|
||||
|
||||
"""
|
||||
# Check for a directory in filename
|
||||
subdir = os.path.dirname(filename) or None
|
||||
if subdir:
|
||||
filename = os.path.basename(filename)
|
||||
|
||||
# Get directory path
|
||||
dirpath = self.get_dir(basedir, subdir, **kwargs)
|
||||
return os.path.join(dirpath, filename)
|
||||
|
||||
|
||||
class MoulinetteError(Exception):
|
||||
"""Moulinette base exception
|
||||
|
||||
Keyword arguments:
|
||||
- code -- Integer error code
|
||||
- message -- Error message to display
|
||||
|
||||
"""
|
||||
def __init__(self, code, message):
|
||||
self.code = code
|
||||
self.message = message
|
||||
|
||||
errorcode_desc = {
|
||||
1 : _('Fail'),
|
||||
13 : _('Permission denied'),
|
||||
17 : _('Already exists'),
|
||||
22 : _('Invalid arguments'),
|
||||
87 : _('Too many users'),
|
||||
111 : _('Connection refused'),
|
||||
122 : _('Quota exceeded'),
|
||||
125 : _('Operation canceled'),
|
||||
167 : _('Not found'),
|
||||
168 : _('Undefined'),
|
||||
169 : _('LDAP operation error')
|
||||
}
|
||||
if code in errorcode_desc:
|
||||
self.desc = errorcode_desc[code]
|
||||
else:
|
||||
self.desc = _('Error %s' % code)
|
||||
|
||||
def __str__(self, colorized=False):
|
||||
desc = self.desc
|
||||
if colorized:
|
||||
desc = colorize(self.desc, 'red')
|
||||
return _('%s: %s' % (desc, self.message))
|
||||
|
||||
def colorize(self):
|
||||
return self.__str__(colorized=True)
|
|
@ -1,503 +0,0 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import argparse
|
||||
import pickle
|
||||
import yaml
|
||||
import re
|
||||
import os
|
||||
from collections import OrderedDict
|
||||
|
||||
import logging
|
||||
|
||||
from .. import __version__
|
||||
from ..config import actionsmap_path, actionsmap_cache_path
|
||||
|
||||
from extraparameters import extraparameters_list
|
||||
from helpers import Interface, YunoHostError
|
||||
|
||||
## Additional parsers
|
||||
|
||||
class _HTTPArgumentParser(object):
|
||||
|
||||
def __init__(self, method, uri):
|
||||
# Initialize the ArgumentParser object
|
||||
self._parser = argparse.ArgumentParser(usage='',
|
||||
prefix_chars='@',
|
||||
add_help=False)
|
||||
self._parser.error = self._error
|
||||
|
||||
self.method = method
|
||||
self.uri = uri
|
||||
|
||||
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):
|
||||
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)
|
||||
|
||||
def _error(self, message):
|
||||
# TODO: Raise a proper exception
|
||||
raise Exception(message)
|
||||
|
||||
class HTTPParser(object):
|
||||
"""
|
||||
Object for parsing HTTP requests into Python objects.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._parsers = {} # dict({(method, uri): _HTTPArgumentParser})
|
||||
|
||||
@property
|
||||
def routes(self):
|
||||
"""Get current routes"""
|
||||
return self._parsers.keys()
|
||||
|
||||
def add_parser(self, method, uri):
|
||||
"""
|
||||
Add a parser for a given route
|
||||
|
||||
Keyword arguments:
|
||||
- method -- The route's HTTP method (GET, POST, PUT, DELETE)
|
||||
- uri -- The route's URI
|
||||
|
||||
Returns:
|
||||
A new _HTTPArgumentParser object for the route
|
||||
|
||||
"""
|
||||
# Check if a parser already exists for the route
|
||||
key = (method, uri)
|
||||
if key in self.routes:
|
||||
raise ValueError("A parser for '%s' already exists" % key)
|
||||
|
||||
# Create and append parser
|
||||
parser = _HTTPArgumentParser(method, uri)
|
||||
self._parsers[key] = parser
|
||||
|
||||
# Return the created parser
|
||||
return parser
|
||||
|
||||
def parse_args(self, method, uri, args={}):
|
||||
"""
|
||||
Convert argument variables to objects and assign them as
|
||||
attributes of the namespace for a given route
|
||||
|
||||
Keyword arguments:
|
||||
- method -- The route's HTTP method (GET, POST, PUT, DELETE)
|
||||
- uri -- The route's URI
|
||||
- args -- Argument variables for the route
|
||||
|
||||
Returns:
|
||||
The populated namespace
|
||||
|
||||
"""
|
||||
# Retrieve the parser for the route
|
||||
key = (method, uri)
|
||||
if key not in self.routes:
|
||||
raise ValueError("No parser for '%s %s' found" % key)
|
||||
|
||||
return self._parsers[key].parse_args(args)
|
||||
|
||||
class ExtraParser(object):
|
||||
"""
|
||||
Global parser for the extra parameters.
|
||||
|
||||
"""
|
||||
def __init__(self, iface):
|
||||
self.iface = iface
|
||||
self.extra = OrderedDict()
|
||||
|
||||
# Append available extra parameters for the current interface
|
||||
for klass in extraparameters_list:
|
||||
if iface in klass.skipped_iface:
|
||||
continue
|
||||
if klass.name in self.extra:
|
||||
logging.warning("extra parameter named '%s' was already added" % klass.name)
|
||||
continue
|
||||
self.extra[klass.name] = klass
|
||||
|
||||
def validate(self, arg_name, parameters):
|
||||
"""
|
||||
Validate values of extra parameters for an argument
|
||||
|
||||
Keyword arguments:
|
||||
- arg_name -- The argument name
|
||||
- parameters -- A dict of extra parameters with their values
|
||||
|
||||
"""
|
||||
# Iterate over parameters to validate
|
||||
for p, v in parameters.items():
|
||||
# Remove unknow parameters
|
||||
if p not in self.extra.keys():
|
||||
del parameters[p]
|
||||
|
||||
# Validate parameter value
|
||||
parameters[p] = self.extra[p].validate(v, arg_name)
|
||||
|
||||
return parameters
|
||||
|
||||
def parse(self, arg_name, arg_value, parameters):
|
||||
"""
|
||||
Parse argument with extra parameters
|
||||
|
||||
Keyword arguments:
|
||||
- arg_name -- The argument name
|
||||
- arg_value -- The argument value
|
||||
- parameters -- A dict of extra parameters with their values
|
||||
|
||||
"""
|
||||
# Iterate over available parameters
|
||||
for p, klass in self.extra.items():
|
||||
if p not in parameters.keys():
|
||||
continue
|
||||
|
||||
# Initialize the extra parser
|
||||
parser = klass(self.iface)
|
||||
|
||||
# Parse the argument
|
||||
if isinstance(arg_value, list):
|
||||
for v in arg_value:
|
||||
r = parser(parameters[p], arg_name, v)
|
||||
if r not in arg_value:
|
||||
arg_value.append(r)
|
||||
else:
|
||||
arg_value = parser(parameters[p], arg_name, arg_value)
|
||||
|
||||
return arg_value
|
||||
|
||||
|
||||
## Main class
|
||||
|
||||
class ActionsMap(object):
|
||||
"""
|
||||
Validate and process action defined into the actions map.
|
||||
|
||||
The actions map defines features and their usage of the main
|
||||
application. It is composed by categories which contain one or more
|
||||
action(s). Moreover, the action can have specific argument(s).
|
||||
|
||||
Keyword arguments:
|
||||
|
||||
- interface -- Interface type that requires the actions map.
|
||||
Possible value is one of:
|
||||
- 'cli' for the command line interface
|
||||
- 'api' for an API usage (HTTP requests)
|
||||
|
||||
- use_cache -- False if it should parse the actions map file
|
||||
instead of using the cached one.
|
||||
|
||||
"""
|
||||
def __init__(self, interface, use_cache=True):
|
||||
if interface not in Interface.all():
|
||||
raise ValueError(_("Invalid interface '%s'" % interface))
|
||||
self.interface = interface
|
||||
self.use_cache = use_cache
|
||||
|
||||
logging.debug("initializing ActionsMap for the '%s' interface" % interface)
|
||||
|
||||
# Iterate over actions map namespaces
|
||||
actionsmaps = {}
|
||||
for n in self.get_actionsmap_namespaces():
|
||||
logging.debug("loading '%s' actions map namespace" % n)
|
||||
|
||||
if use_cache:
|
||||
# Attempt to load cache if it exists
|
||||
cache_file = '%s/%s.pkl' % (actionsmap_cache_path, n)
|
||||
if os.path.isfile(cache_file):
|
||||
with open(cache_file, 'r') as f:
|
||||
actionsmaps[n] = pickle.load(f)
|
||||
else:
|
||||
self.use_cache = False
|
||||
actionsmaps = self.generate_cache()
|
||||
break
|
||||
else:
|
||||
am_file = '%s/%s.yml' % (actionsmap_path, n)
|
||||
with open(am_file, 'r') as f:
|
||||
actionsmaps[n] = yaml.load(f)
|
||||
|
||||
# Generate parsers
|
||||
self.extraparser = ExtraParser(interface)
|
||||
self.parser = self._construct_parser(actionsmaps)
|
||||
|
||||
def process(self, args, route=None):
|
||||
"""
|
||||
Parse arguments and process the proper action
|
||||
|
||||
Keyword arguments:
|
||||
- args -- The arguments to parse
|
||||
- route -- A tupple (method, uri) of the requested route (api only)
|
||||
|
||||
"""
|
||||
arguments = None
|
||||
|
||||
# Parse arguments
|
||||
if self.interface == Interface.cli:
|
||||
arguments = self.parser.parse_args(args)
|
||||
elif self.interface == Interface.api:
|
||||
if route is None:
|
||||
# TODO: Raise a proper exception
|
||||
raise Exception(_("Missing route argument"))
|
||||
method, uri = route
|
||||
arguments = self.parser.parse_args(method, uri, args)
|
||||
arguments = vars(arguments)
|
||||
|
||||
# Parse extra parameters
|
||||
arguments = self._parse_extra_parameters(arguments)
|
||||
|
||||
# Retrieve action information
|
||||
namespace = arguments['_info']['namespace']
|
||||
category = arguments['_info']['category']
|
||||
action = arguments['_info']['action']
|
||||
del arguments['_info']
|
||||
|
||||
module = '%s.%s' % (namespace, category)
|
||||
function = '%s_%s' % (category, action)
|
||||
|
||||
try:
|
||||
mod = __import__(module, globals=globals(), fromlist=[function], level=0)
|
||||
func = getattr(mod, function)
|
||||
except (AttributeError, ImportError):
|
||||
raise YunoHostError(168, _('Function is not defined'))
|
||||
else:
|
||||
# Process the action
|
||||
return func(**arguments)
|
||||
|
||||
@staticmethod
|
||||
def get_actionsmap_namespaces(path=actionsmap_path):
|
||||
"""
|
||||
Retrieve actions map namespaces from a given path
|
||||
|
||||
Returns:
|
||||
A list of available namespaces
|
||||
|
||||
"""
|
||||
namespaces = []
|
||||
|
||||
for f in os.listdir(path):
|
||||
if f.endswith('.yml'):
|
||||
namespaces.append(f[:-4])
|
||||
return namespaces
|
||||
|
||||
@classmethod
|
||||
def generate_cache(klass):
|
||||
"""
|
||||
Generate cache for the actions map's file(s)
|
||||
|
||||
Returns:
|
||||
A dict of actions map for each namespaces
|
||||
|
||||
"""
|
||||
actionsmaps = {}
|
||||
|
||||
if not os.path.isdir(actionsmap_cache_path):
|
||||
os.makedirs(actionsmap_cache_path)
|
||||
|
||||
# Iterate over actions map namespaces
|
||||
for n in klass.get_actionsmap_namespaces():
|
||||
logging.debug("generating cache for '%s' actions map namespace" % n)
|
||||
|
||||
# Read actions map from yaml file
|
||||
am_file = '%s/%s.yml' % (actionsmap_path, n)
|
||||
with open(am_file, 'r') as f:
|
||||
actionsmaps[n] = yaml.load(f)
|
||||
|
||||
# Cache actions map into pickle file
|
||||
cache_file = '%s/%s.pkl' % (actionsmap_cache_path, n)
|
||||
with open(cache_file, 'w') as f:
|
||||
pickle.dump(actionsmaps[n], f)
|
||||
|
||||
return actionsmaps
|
||||
|
||||
|
||||
## Private class and methods
|
||||
|
||||
def _store_extra_parameters(self, parser, arg_name, arg_params):
|
||||
"""
|
||||
Store extra parameters for a given argument
|
||||
|
||||
Keyword arguments:
|
||||
- parser -- Parser object for the arguments
|
||||
- arg_name -- Argument name
|
||||
- arg_params -- Argument parameters
|
||||
|
||||
Returns:
|
||||
The parser object
|
||||
|
||||
"""
|
||||
if 'extra' in arg_params:
|
||||
# Retrieve current extra parameters dict
|
||||
extra = parser.get_default('_extra')
|
||||
if not extra or not isinstance(extra, dict):
|
||||
extra = {}
|
||||
|
||||
if not self.use_cache:
|
||||
# Validate extra parameters for the argument
|
||||
extra[arg_name] = self.extraparser.validate(arg_name, arg_params['extra'])
|
||||
else:
|
||||
extra[arg_name] = arg_params['extra']
|
||||
parser.set_defaults(_extra=extra)
|
||||
|
||||
return parser
|
||||
|
||||
def _parse_extra_parameters(self, args):
|
||||
"""
|
||||
Parse arguments with their extra parameters
|
||||
|
||||
Keyword arguments:
|
||||
- args -- A dict of all arguments
|
||||
|
||||
Return:
|
||||
The parsed arguments dict
|
||||
|
||||
"""
|
||||
# Retrieve extra parameters from the arguments
|
||||
if '_extra' not in args:
|
||||
return args
|
||||
extra = args['_extra']
|
||||
del args['_extra']
|
||||
|
||||
# Validate extra parameters for each arguments
|
||||
for an, parameters in extra.items():
|
||||
args[an] = self.extraparser.parse(an, args[an], parameters)
|
||||
|
||||
return args
|
||||
|
||||
def _construct_parser(self, actionsmaps):
|
||||
"""
|
||||
Construct the parser with the actions map
|
||||
|
||||
Keyword arguments:
|
||||
- actionsmaps -- A dict of multi-level dictionnary of
|
||||
categories/actions/arguments list for each namespaces
|
||||
|
||||
Returns:
|
||||
An interface relevant's parser object
|
||||
|
||||
"""
|
||||
top_parser = None
|
||||
iface = self.interface
|
||||
|
||||
# Create parser object
|
||||
if iface == Interface.cli:
|
||||
# TODO: Add descritpion (from __description__?)
|
||||
top_parser = argparse.ArgumentParser()
|
||||
top_subparsers = top_parser.add_subparsers()
|
||||
elif iface == Interface.api:
|
||||
top_parser = HTTPParser()
|
||||
|
||||
## Format option strings from argument parameters
|
||||
def _option_strings(arg_name, arg_params):
|
||||
if iface == Interface.cli:
|
||||
if arg_name[0] == '-' and 'full' in arg_params:
|
||||
return [arg_name, arg_params['full']]
|
||||
return [arg_name]
|
||||
elif iface == Interface.api:
|
||||
if arg_name[0] != '-':
|
||||
return [arg_name]
|
||||
if 'full' in arg_params:
|
||||
return [arg_params['full'].replace('--', '@', 1)]
|
||||
if arg_name.startswith('--'):
|
||||
return [arg_name.replace('--', '@', 1)]
|
||||
return [arg_name.replace('-', '@', 1)]
|
||||
|
||||
## Remove extra parameters
|
||||
def _clean_params(arg_params):
|
||||
for k in {'full', 'extra'}:
|
||||
if k in arg_params:
|
||||
del arg_params[k]
|
||||
return arg_params
|
||||
|
||||
# Iterate over actions map namespaces
|
||||
for n, actionsmap in actionsmaps.items():
|
||||
# Parse general arguments for the cli only
|
||||
if iface == Interface.cli:
|
||||
for an, ap in actionsmap['general_arguments'].items():
|
||||
if 'version' in ap:
|
||||
ap['version'] = ap['version'].replace('%version%', __version__)
|
||||
top_parser.add_argument(*_option_strings(an, ap), **_clean_params(ap))
|
||||
del actionsmap['general_arguments']
|
||||
|
||||
# Parse categories
|
||||
for cn, cp in actionsmap.items():
|
||||
if 'actions' not in cp:
|
||||
continue
|
||||
|
||||
# Add category subparsers for the cli only
|
||||
if iface == Interface.cli:
|
||||
c_help = cp.get('category_help')
|
||||
subparsers = top_subparsers.add_parser(cn, help=c_help).add_subparsers()
|
||||
|
||||
# Parse actions
|
||||
for an, ap in cp['actions'].items():
|
||||
parser = None
|
||||
|
||||
# Add parser for the current action
|
||||
if iface == Interface.cli:
|
||||
a_help = ap.get('action_help')
|
||||
parser = subparsers.add_parser(an, help=a_help)
|
||||
elif iface == Interface.api and 'api' in ap:
|
||||
# Extract method and uri
|
||||
m = re.match('(GET|POST|PUT|DELETE) (/\S+)', ap['api'])
|
||||
if m:
|
||||
parser = top_parser.add_parser(m.group(1), m.group(2))
|
||||
if not parser:
|
||||
continue
|
||||
|
||||
# Store action information
|
||||
parser.set_defaults(_info={'namespace': n,
|
||||
'category': cn,
|
||||
'action': an})
|
||||
|
||||
# Add arguments
|
||||
if not 'arguments' in ap:
|
||||
continue
|
||||
for argn, argp in ap['arguments'].items():
|
||||
arg = parser.add_argument(*_option_strings(argn, argp),
|
||||
**_clean_params(argp.copy()))
|
||||
parser = self._store_extra_parameters(parser, arg.dest, argp)
|
||||
|
||||
return top_parser
|
0
src/moulinette/core/__init__.py → src/moulinette/extra/__init__.py
Executable file → Normal file
0
src/moulinette/core/__init__.py → src/moulinette/extra/__init__.py
Executable file → Normal file
|
@ -4,7 +4,8 @@ import getpass
|
|||
import re
|
||||
import logging
|
||||
|
||||
from helpers import Interface, colorize, YunoHostError
|
||||
from .. import MoulinetteError
|
||||
from ..helpers import colorize
|
||||
|
||||
class _ExtraParameter(object):
|
||||
"""
|
||||
|
@ -78,7 +79,7 @@ class AskParameter(_ExtraParameter):
|
|||
|
||||
"""
|
||||
name = 'ask'
|
||||
skipped_iface = {Interface.api}
|
||||
skipped_iface = { 'api' }
|
||||
|
||||
def __call__(self, message, arg_name, arg_value):
|
||||
# TODO: Fix asked arguments ordering
|
||||
|
@ -119,7 +120,7 @@ class PasswordParameter(AskParameter):
|
|||
pwd1 = getpass.getpass(colorize(message + ': ', 'cyan'))
|
||||
pwd2 = getpass.getpass(colorize('Retype ' + message + ': ', 'cyan'))
|
||||
if pwd1 != pwd2:
|
||||
raise YunoHostError(22, _("Passwords don't match"))
|
||||
raise MoulinetteError(22, _("Passwords don't match"))
|
||||
return pwd1
|
||||
|
||||
class PatternParameter(_ExtraParameter):
|
||||
|
@ -137,7 +138,7 @@ class PatternParameter(_ExtraParameter):
|
|||
message = arguments[1]
|
||||
|
||||
if arg_value is not None and not re.match(pattern, arg_value):
|
||||
raise YunoHostError(22, message)
|
||||
raise MoulinetteError(22, message)
|
||||
return arg_value
|
||||
|
||||
@staticmethod
|
|
@ -21,21 +21,6 @@ import getpass
|
|||
if not __debug__:
|
||||
import traceback
|
||||
|
||||
|
||||
class Interface():
|
||||
"""
|
||||
Contain available interfaces to use with the moulinette.
|
||||
|
||||
"""
|
||||
api = 'api'
|
||||
cli = 'cli'
|
||||
|
||||
@classmethod
|
||||
def all(klass):
|
||||
"""Get a list of all interfaces"""
|
||||
ifaces = set(i for i in dir(klass) if not i.startswith('_'))
|
||||
return ifaces
|
||||
|
||||
win = []
|
||||
|
||||
def random_password(length=8):
|
||||
|
@ -105,33 +90,6 @@ def win_msg(astr):
|
|||
win.append(astr)
|
||||
|
||||
|
||||
def str_to_func(astr):
|
||||
"""
|
||||
Call a function from a string name
|
||||
|
||||
Keyword arguments:
|
||||
astr -- Name of function to call
|
||||
|
||||
Returns:
|
||||
Function
|
||||
|
||||
"""
|
||||
try:
|
||||
module, _, function = astr.rpartition('.')
|
||||
if module:
|
||||
__import__(module)
|
||||
mod = sys.modules[module]
|
||||
else:
|
||||
mod = sys.modules['__main__'] # default module
|
||||
|
||||
func = getattr(mod, function)
|
||||
except (AttributeError, ImportError):
|
||||
#raise YunoHostError(168, _('Function is not defined'))
|
||||
return None
|
||||
else:
|
||||
return func
|
||||
|
||||
|
||||
def validate(pattern, array):
|
||||
"""
|
||||
Validate attributes with a pattern
|
||||
|
@ -441,115 +399,3 @@ class YunoHostLDAP(Singleton):
|
|||
else:
|
||||
raise YunoHostError(17, _('Attribute already exists') + ' "' + attr + '=' + value + '"')
|
||||
return True
|
||||
|
||||
|
||||
def parse_dict(action_map):
|
||||
"""
|
||||
Turn action dictionnary to parser, subparsers and arguments
|
||||
|
||||
Keyword arguments:
|
||||
action_map -- Multi-level dictionnary of categories/actions/arguments list
|
||||
|
||||
Returns:
|
||||
Namespace of args
|
||||
|
||||
"""
|
||||
# Intialize parsers
|
||||
parsers = subparsers_category = subparsers_action = {}
|
||||
parsers['general'] = argparse.ArgumentParser()
|
||||
subparsers = parsers['general'].add_subparsers()
|
||||
new_args = []
|
||||
patterns = {}
|
||||
|
||||
# Add general arguments
|
||||
for arg_name, arg_params in action_map['general_arguments'].items():
|
||||
if 'version' in arg_params:
|
||||
v = arg_params['version']
|
||||
arg_params['version'] = v.replace('%version%', __version__)
|
||||
if 'full' in arg_params:
|
||||
arg_names = [arg_name, arg_params['full']]
|
||||
arg_fullname = arg_params['full']
|
||||
del arg_params['full']
|
||||
else: arg_names = [arg_name]
|
||||
parsers['general'].add_argument(*arg_names, **arg_params)
|
||||
|
||||
del action_map['general_arguments']
|
||||
|
||||
# Split categories into subparsers
|
||||
for category, category_params in action_map.items():
|
||||
if 'category_help' not in category_params: category_params['category_help'] = ''
|
||||
subparsers_category[category] = subparsers.add_parser(category, help=category_params['category_help'])
|
||||
subparsers_action[category] = subparsers_category[category].add_subparsers()
|
||||
# Split actions
|
||||
if 'actions' in category_params:
|
||||
for action, action_params in category_params['actions'].items():
|
||||
if 'action_help' not in action_params: action_params['action_help'] = ''
|
||||
parsers[category + '_' + action] = subparsers_action[category].add_parser(action, help=action_params['action_help'])
|
||||
# Set the action s related function
|
||||
parsers[category + '_' + action].set_defaults(
|
||||
func=str_to_func('yunohost_' + category + '.'
|
||||
+ category + '_' + action.replace('-', '_')))
|
||||
# Add arguments
|
||||
if 'arguments' in action_params:
|
||||
for arg_name, arg_params in action_params['arguments'].items():
|
||||
arg_fullname = False
|
||||
|
||||
if 'password' in arg_params:
|
||||
if arg_params['password']: is_password = True
|
||||
del arg_params['password']
|
||||
else: is_password = False
|
||||
|
||||
if 'full' in arg_params:
|
||||
arg_names = [arg_name, arg_params['full']]
|
||||
arg_fullname = arg_params['full']
|
||||
del arg_params['full']
|
||||
else: arg_names = [arg_name]
|
||||
|
||||
if 'ask' in arg_params:
|
||||
require_input = True
|
||||
if '-h' in sys.argv or '--help' in sys.argv:
|
||||
require_input = False
|
||||
if (category != sys.argv[1]) or (action != sys.argv[2]):
|
||||
require_input = False
|
||||
for name in arg_names:
|
||||
if name in sys.argv[2:]: require_input = False
|
||||
|
||||
if require_input:
|
||||
if is_password:
|
||||
if os.isatty(1):
|
||||
pwd1 = getpass.getpass(colorize(arg_params['ask'] + ': ', 'cyan'))
|
||||
pwd2 = getpass.getpass(colorize('Retype ' + arg_params['ask'][0].lower() + arg_params['ask'][1:] + ': ', 'cyan'))
|
||||
if pwd1 != pwd2:
|
||||
raise YunoHostError(22, _("Passwords don't match"))
|
||||
sys.exit(1)
|
||||
else:
|
||||
raise YunoHostError(22, _("Missing arguments") + ': ' + arg_name)
|
||||
if arg_name[0] == '-': arg_extend = [arg_name, pwd1]
|
||||
else: arg_extend = [pwd1]
|
||||
else:
|
||||
if os.isatty(1):
|
||||
arg_value = raw_input(colorize(arg_params['ask'] + ': ', 'cyan'))
|
||||
else:
|
||||
raise YunoHostError(22, _("Missing arguments") + ': ' + arg_name)
|
||||
if arg_name[0] == '-': arg_extend = [arg_name, arg_value]
|
||||
else: arg_extend = [arg_value]
|
||||
new_args.extend(arg_extend)
|
||||
del arg_params['ask']
|
||||
|
||||
if 'pattern' in arg_params:
|
||||
if (category == sys.argv[1]) and (action == sys.argv[2]):
|
||||
if 'dest' in arg_params: name = arg_params['dest']
|
||||
elif arg_fullname: name = arg_fullname[2:]
|
||||
else: name = arg_name
|
||||
name = name.replace('-', '_')
|
||||
patterns[name] = arg_params['pattern']
|
||||
del arg_params['pattern']
|
||||
|
||||
parsers[category + '_' + action].add_argument(*arg_names, **arg_params)
|
||||
|
||||
args = parsers['general'].parse_args(sys.argv.extend(new_args))
|
||||
args_dict = vars(args)
|
||||
for key, value in patterns.items():
|
||||
validate(value, args_dict[key])
|
||||
|
||||
return args
|
114
src/moulinette/interface/__init__.py
Executable file
114
src/moulinette/interface/__init__.py
Executable file
|
@ -0,0 +1,114 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
class BaseParser(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.
|
||||
general arguments, categories and actions).
|
||||
|
||||
"""
|
||||
|
||||
## Optional variables
|
||||
# Each parser classes can overwrite these variables.
|
||||
|
||||
"""Either it will parse general arguments, or not"""
|
||||
parse_general = True
|
||||
|
||||
"""Either it will parse categories, or not"""
|
||||
parse_category = True
|
||||
|
||||
"""Either it will parse actions, or not"""
|
||||
parse_action = True
|
||||
|
||||
|
||||
## Virtual methods
|
||||
# Each parser classes can implement these methods.
|
||||
|
||||
@staticmethod
|
||||
def format_arg_name(name, full):
|
||||
"""Format argument name
|
||||
|
||||
Format agument name depending on its 'full' parameters and return
|
||||
a list to use it as option string 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_general_parser(self, **kwargs):
|
||||
"""Add a parser for general arguments
|
||||
|
||||
Create and return an argument parser for general arguments.
|
||||
|
||||
Returns:
|
||||
An ArgumentParser based object
|
||||
|
||||
"""
|
||||
if not self.parse_general:
|
||||
msg = "doesn't parse general arguments"
|
||||
else:
|
||||
msg = "must override this method"
|
||||
raise NotImplementedError("derived class '%s' %s" % \
|
||||
(self.__class__.__name__, msg))
|
||||
|
||||
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
|
||||
|
||||
"""
|
||||
if not self.parse_categories:
|
||||
msg = "doesn't parse categories"
|
||||
else:
|
||||
msg = "must override this method"
|
||||
raise NotImplementedError("derived class '%s' %s" % \
|
||||
(self.__class__.__name__, msg))
|
||||
|
||||
def add_action_parser(self, name, **kwargs):
|
||||
"""Add a parser for an action
|
||||
|
||||
Create a new action and return an argument parser for it.
|
||||
|
||||
Keyword arguments:
|
||||
- name -- The action name
|
||||
|
||||
Returns:
|
||||
An ArgumentParser based object
|
||||
|
||||
"""
|
||||
if not self.parse_general:
|
||||
msg = "doesn't parse actions"
|
||||
else:
|
||||
msg = "must override this method"
|
||||
raise NotImplementedError("derived class '%s' %s" % \
|
||||
(self.__class__.__name__, msg))
|
||||
|
||||
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__)
|
|
@ -1,16 +1,162 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import re
|
||||
import argparse
|
||||
import os.path
|
||||
from bottle import Bottle, request, response, HTTPResponse
|
||||
from beaker.middleware import SessionMiddleware
|
||||
|
||||
from ..config import session_path, doc_json_path
|
||||
from helpers import YunoHostError, YunoHostLDAP
|
||||
from . import BaseParser
|
||||
from .. import MoulinetteError
|
||||
from ..helpers import YunoHostError, YunoHostLDAP
|
||||
|
||||
## API arguments 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):
|
||||
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)
|
||||
|
||||
def _error(self, message):
|
||||
# TODO: Raise a proper exception
|
||||
raise MoulinetteError(1, message)
|
||||
|
||||
class APIParser(BaseParser):
|
||||
"""Actions map's API Parser
|
||||
|
||||
"""
|
||||
parse_category = False
|
||||
parse_general = False
|
||||
|
||||
def __init__(self):
|
||||
self._parsers = {} # dict({(method, path): _HTTPArgumentParser})
|
||||
|
||||
@property
|
||||
def routes(self):
|
||||
"""Get current routes"""
|
||||
return self._parsers.keys()
|
||||
|
||||
|
||||
## Bottle Plugins
|
||||
## Implement virtual methods
|
||||
|
||||
class APIAuthPlugin(object):
|
||||
@staticmethod
|
||||
def format_arg_name(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_action_parser(self, name, 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
|
||||
|
||||
"""
|
||||
if not api:
|
||||
return None
|
||||
|
||||
# Validate action route
|
||||
m = re.match('(GET|POST|PUT|DELETE) (/\S+)', api)
|
||||
if not m:
|
||||
return None
|
||||
|
||||
# Check if a parser already exists for the route
|
||||
key = (m.group(1), m.group(2))
|
||||
if key in self.routes:
|
||||
raise ValueError("A parser for '%s' already exists" % key)
|
||||
|
||||
# Create and append parser
|
||||
parser = _HTTPArgumentParser()
|
||||
self._parsers[key] = parser
|
||||
|
||||
# Return the created parser
|
||||
return parser
|
||||
|
||||
def parse_args(self, args, route, **kwargs):
|
||||
"""Parse arguments
|
||||
|
||||
Keyword arguments:
|
||||
- route -- The action route (e.g. 'GET /' )
|
||||
|
||||
"""
|
||||
# Retrieve the parser for the route
|
||||
if route not in self.routes:
|
||||
raise MoulinetteError(22, "No parser for '%s %s' found" % key)
|
||||
|
||||
return self._parsers[route].parse_args(args)
|
||||
|
||||
actionsmap_parser = APIParser
|
||||
|
||||
|
||||
## API moulinette interface
|
||||
|
||||
class _APIAuthPlugin(object):
|
||||
"""
|
||||
Manage the authentication for the API access.
|
||||
|
||||
|
@ -20,8 +166,7 @@ class APIAuthPlugin(object):
|
|||
|
||||
def __init__(self):
|
||||
# TODO: Add options (e.g. session type, content type, ...)
|
||||
if not os.path.isdir(session_path):
|
||||
os.makedirs(session_path)
|
||||
pass
|
||||
|
||||
@property
|
||||
def app(self):
|
||||
|
@ -44,7 +189,7 @@ class APIAuthPlugin(object):
|
|||
session_opts = {
|
||||
'session.type': 'file',
|
||||
'session.cookie_expires': True,
|
||||
'session.data_dir': session_path,
|
||||
'session.data_dir': pkg.cachedir('session', make_dir=True),
|
||||
'session.secure': True
|
||||
}
|
||||
self._app = SessionMiddleware(app, session_opts)
|
||||
|
@ -119,7 +264,7 @@ class APIAuthPlugin(object):
|
|||
return True
|
||||
return False
|
||||
|
||||
class ActionsMapPlugin(object):
|
||||
class _ActionsMapPlugin(object):
|
||||
"""
|
||||
Process action for the request using the actions map.
|
||||
|
||||
|
@ -158,19 +303,15 @@ class ActionsMapPlugin(object):
|
|||
return wrapper
|
||||
|
||||
|
||||
## Main class
|
||||
|
||||
class MoulinetteAPI(object):
|
||||
"""
|
||||
Initialize a HTTP server which serves the API to access to the
|
||||
moulinette actions.
|
||||
|
||||
Keyword arguments:
|
||||
|
||||
- actionsmap -- The relevant ActionsMap instance
|
||||
|
||||
- routes -- A dict of additional routes to add in the form of
|
||||
{(method, uri): callback}
|
||||
{(method, path): callback}
|
||||
|
||||
"""
|
||||
|
||||
|
@ -182,14 +323,14 @@ class MoulinetteAPI(object):
|
|||
callback=self.doc, skip=['apiauth'])
|
||||
|
||||
# Append routes from the actions map
|
||||
amap = ActionsMapPlugin(actionsmap)
|
||||
for (m, u) in actionsmap.parser.routes:
|
||||
app.route(u, method=m, callback=self._error, apply=amap)
|
||||
amap = _ActionsMapPlugin(actionsmap)
|
||||
for (m, p) in actionsmap.parser.routes:
|
||||
app.route(p, method=m, callback=self._error, apply=amap)
|
||||
|
||||
# Append additional routes
|
||||
# TODO: Add an option to skip auth for the route
|
||||
for (m, u), c in routes.items():
|
||||
app.route(u, method=m, callback=c)
|
||||
for (m, p), c in routes.items():
|
||||
app.route(p, method=m, callback=c)
|
||||
|
||||
# Define and install a plugin which sets proper header
|
||||
def apiheader(callback):
|
||||
|
@ -201,7 +342,7 @@ class MoulinetteAPI(object):
|
|||
app.install(apiheader)
|
||||
|
||||
# Install authentication plugin
|
||||
apiauth = APIAuthPlugin()
|
||||
apiauth = _APIAuthPlugin()
|
||||
app.install(apiauth)
|
||||
|
||||
self._app = apiauth.app
|
||||
|
@ -220,11 +361,11 @@ class MoulinetteAPI(object):
|
|||
|
||||
"""
|
||||
if category is None:
|
||||
with open(doc_json_path +'/resources.json') as f:
|
||||
with open(pkg.datafile('doc/resources.json')) as f:
|
||||
return f.read()
|
||||
|
||||
try:
|
||||
with open(doc_json_path +'/'+ category +'.json') as f:
|
||||
with open(pkg.datafile('doc/%s.json' % category)) as f:
|
||||
return f.read()
|
||||
except IOError:
|
||||
return 'unknown'
|
35
src/moulinette/interface/cli.py
Normal file
35
src/moulinette/interface/cli.py
Normal file
|
@ -0,0 +1,35 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import argparse
|
||||
from . import BaseParser
|
||||
|
||||
## CLI arguments Parser
|
||||
|
||||
class CLIParser(BaseParser):
|
||||
"""Actions map's CLI Parser
|
||||
|
||||
"""
|
||||
def __init__(self, parser=None):
|
||||
self._parser = parser or argparse.ArgumentParser()
|
||||
self._subparsers = self._parser.add_subparsers()
|
||||
|
||||
@staticmethod
|
||||
def format_arg_name(name, full):
|
||||
if name[0] == '-' and full:
|
||||
return [name, full]
|
||||
return [name]
|
||||
|
||||
def add_general_parser(self, **kwargs):
|
||||
return self._parser
|
||||
|
||||
def add_category_parser(self, name, category_help=None, **kwargs):
|
||||
parser = self._subparsers.add_parser(name, help=category_help)
|
||||
return CLIParser(parser)
|
||||
|
||||
def add_action_parser(self, name, action_help, **kwargs):
|
||||
return self._subparsers.add_parser(name, help=action_help)
|
||||
|
||||
def parse_args(self, args, **kwargs):
|
||||
return self._parser.parse_args(args)
|
||||
|
||||
actionsmap_parser = CLIParser
|
|
@ -37,7 +37,7 @@ from domain import domain_list, domain_add
|
|||
from user import user_info, user_list
|
||||
from hook import hook_exec, hook_add, hook_remove
|
||||
|
||||
from .moulinette.core.helpers import YunoHostError, YunoHostLDAP, win_msg, random_password, is_true, validate
|
||||
from moulinette.helpers import YunoHostError, YunoHostLDAP, win_msg, random_password, is_true, validate
|
||||
|
||||
repo_path = '/var/cache/yunohost/repo'
|
||||
apps_path = '/usr/share/yunohost/apps'
|
||||
|
|
|
@ -29,7 +29,7 @@ import json
|
|||
import yaml
|
||||
import glob
|
||||
|
||||
from moulinette.core.helpers import YunoHostError, YunoHostLDAP, validate, colorize, win_msg
|
||||
from moulinette.helpers import YunoHostError, YunoHostLDAP, validate, colorize, win_msg
|
||||
|
||||
def backup_init(helper=False):
|
||||
"""
|
||||
|
|
|
@ -34,7 +34,7 @@ import requests
|
|||
from urllib import urlopen
|
||||
from dyndns import dyndns_subscribe
|
||||
|
||||
from moulinette.core.helpers import YunoHostError, YunoHostLDAP, win_msg, colorize, validate, get_required_args
|
||||
from moulinette.helpers import YunoHostError, YunoHostLDAP, win_msg, colorize, validate, get_required_args
|
||||
|
||||
|
||||
def domain_list(filter=None, limit=None, offset=None):
|
||||
|
|
|
@ -30,7 +30,7 @@ import json
|
|||
import glob
|
||||
import base64
|
||||
|
||||
from moulinette.core.helpers import YunoHostError, YunoHostLDAP, validate, colorize, win_msg
|
||||
from moulinette.helpers import YunoHostError, YunoHostLDAP, validate, colorize, win_msg
|
||||
|
||||
def dyndns_subscribe(subscribe_host="dyndns.yunohost.org", domain=None, key=None):
|
||||
"""
|
||||
|
|
|
@ -37,7 +37,7 @@ except ImportError:
|
|||
sys.stderr.write('apt-get install python-yaml\n')
|
||||
sys.exit(1)
|
||||
|
||||
from moulinette.core.helpers import YunoHostError, win_msg
|
||||
from moulinette.helpers import YunoHostError, win_msg
|
||||
|
||||
|
||||
def firewall_allow(protocol=None, port=None, ipv6=None, upnp=False):
|
||||
|
|
|
@ -28,7 +28,7 @@ import sys
|
|||
import re
|
||||
import json
|
||||
|
||||
from moulinette.core.helpers import YunoHostError, YunoHostLDAP, win_msg, colorize
|
||||
from moulinette.helpers import YunoHostError, YunoHostLDAP, win_msg, colorize
|
||||
|
||||
hook_folder = '/usr/share/yunohost/hooks/'
|
||||
|
||||
|
|
|
@ -37,7 +37,7 @@ from datetime import datetime, timedelta
|
|||
from service import (service_enable, service_disable,
|
||||
service_start, service_stop, service_status)
|
||||
|
||||
from moulinette.core.helpers import YunoHostError, win_msg
|
||||
from moulinette.helpers import YunoHostError, win_msg
|
||||
|
||||
glances_uri = 'http://127.0.0.1:61209'
|
||||
stats_path = '/var/lib/yunohost/stats'
|
||||
|
|
|
@ -28,7 +28,7 @@ import glob
|
|||
import subprocess
|
||||
import os.path
|
||||
|
||||
from moulinette.core.helpers import YunoHostError, win_msg
|
||||
from moulinette.helpers import YunoHostError, win_msg
|
||||
|
||||
|
||||
def service_start(names):
|
||||
|
|
|
@ -36,7 +36,7 @@ from dyndns import dyndns_subscribe
|
|||
from backup import backup_init
|
||||
from app import app_ssowatconf
|
||||
|
||||
from moulinette.core.helpers import YunoHostError, YunoHostLDAP, validate, colorize, get_required_args, win_msg
|
||||
from moulinette.helpers import YunoHostError, YunoHostLDAP, validate, colorize, get_required_args, win_msg
|
||||
|
||||
|
||||
def tools_ldapinit(password=None):
|
||||
|
|
|
@ -33,7 +33,7 @@ import getpass
|
|||
from domain import domain_list
|
||||
from hook import hook_callback
|
||||
|
||||
from moulinette.core.helpers import YunoHostError, YunoHostLDAP, win_msg, colorize, validate, get_required_args
|
||||
from moulinette.helpers import YunoHostError, YunoHostLDAP, win_msg, colorize, validate, get_required_args
|
||||
|
||||
def user_list(fields=None, filter=None, limit=None, offset=None):
|
||||
"""
|
||||
|
|
Loading…
Add table
Reference in a new issue