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 os.path
|
||||||
import gettext
|
import gettext
|
||||||
|
|
||||||
# Debug option
|
# Run from source
|
||||||
if '--debug' in sys.argv:
|
basedir = os.path.abspath(os.path.dirname(__file__) +'/../')
|
||||||
sys.path.append(os.path.abspath(os.path.dirname(__file__) +'/../src'))
|
if os.path.isdir(basedir +'/src'):
|
||||||
from moulinette import cli
|
sys.path.append(basedir +'/src')
|
||||||
from moulinette.core.helpers import YunoHostError, colorize
|
|
||||||
|
|
||||||
gettext.install('YunoHost')
|
from moulinette import init, cli, MoulinetteError
|
||||||
|
from moulinette.helpers import YunoHostError, colorize
|
||||||
|
|
||||||
|
gettext.install('yunohost')
|
||||||
|
|
||||||
|
|
||||||
## Main action
|
## Main action
|
||||||
|
|
||||||
if __name__ == '__main__':
|
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
|
# Additional arguments
|
||||||
use_cache = True
|
use_cache = True
|
||||||
if '--no-cache' in sys.argv:
|
if '--no-cache' in sys.argv:
|
||||||
use_cache = False
|
use_cache = False
|
||||||
sys.argv.remove('--no-cache')
|
sys.argv.remove('--no-cache')
|
||||||
if '--debug' in sys.argv:
|
|
||||||
sys.argv.remove('--debug')
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
args = list(sys.argv)
|
args = list(sys.argv)
|
||||||
|
@ -36,6 +40,9 @@ if __name__ == '__main__':
|
||||||
|
|
||||||
# Execute the action
|
# Execute the action
|
||||||
cli(args, use_cache)
|
cli(args, use_cache)
|
||||||
|
except MoulinetteError as e:
|
||||||
|
print(e.colorize())
|
||||||
|
sys.exit(e.code)
|
||||||
except YunoHostError as e:
|
except YunoHostError as e:
|
||||||
print(colorize(_("Error: "), 'red') + e.message)
|
print(colorize(_("Error: "), 'red') + e.message)
|
||||||
sys.exit(e.code)
|
sys.exit(e.code)
|
||||||
|
|
|
@ -5,12 +5,14 @@ import sys
|
||||||
import os.path
|
import os.path
|
||||||
import gettext
|
import gettext
|
||||||
|
|
||||||
# Debug option
|
# Run from source
|
||||||
if '--debug' in sys.argv:
|
basedir = os.path.abspath(os.path.dirname(__file__) +'/../')
|
||||||
sys.path.append(os.path.abspath(os.path.dirname(__file__) +'/../src'))
|
if os.path.isdir(basedir +'/src'):
|
||||||
from moulinette import api
|
sys.path.append(basedir +'/src')
|
||||||
|
|
||||||
gettext.install('YunoHost')
|
from moulinette import init, api
|
||||||
|
|
||||||
|
gettext.install('yunohost')
|
||||||
|
|
||||||
|
|
||||||
## Callbacks for additional routes
|
## Callbacks for additional routes
|
||||||
|
@ -29,6 +31,10 @@ def is_installed():
|
||||||
## Main action
|
## Main action
|
||||||
|
|
||||||
if __name__ == '__main__':
|
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
|
# Additional arguments
|
||||||
use_cache = True
|
use_cache = True
|
||||||
if '--no-cache' in sys.argv:
|
if '--no-cache' in sys.argv:
|
||||||
|
|
|
@ -76,7 +76,9 @@ user:
|
||||||
help: Must be unique
|
help: Must be unique
|
||||||
extra:
|
extra:
|
||||||
ask: "Username"
|
ask: "Username"
|
||||||
pattern: '^[a-z0-9_]+$'
|
pattern:
|
||||||
|
- '^[a-z0-9_]+$'
|
||||||
|
- "Must be alphanumeric and underscore characters only"
|
||||||
-f:
|
-f:
|
||||||
full: --firstname
|
full: --firstname
|
||||||
extra:
|
extra:
|
||||||
|
@ -109,7 +111,9 @@ user:
|
||||||
nargs: "*"
|
nargs: "*"
|
||||||
extra:
|
extra:
|
||||||
ask: "Users to delete"
|
ask: "Users to delete"
|
||||||
pattern: '^[a-z0-9_]+$'
|
pattern:
|
||||||
|
- '^[a-z0-9_]+$'
|
||||||
|
- "Must be alphanumeric and underscore characters only"
|
||||||
--purge:
|
--purge:
|
||||||
action: store_true
|
action: store_true
|
||||||
|
|
||||||
|
@ -187,7 +191,9 @@ domain:
|
||||||
help: Domain name to add
|
help: Domain name to add
|
||||||
nargs: '+'
|
nargs: '+'
|
||||||
extra:
|
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:
|
-m:
|
||||||
full: --main
|
full: --main
|
||||||
help: Is the main domain
|
help: Is the main domain
|
||||||
|
@ -206,7 +212,9 @@ domain:
|
||||||
help: Domain(s) to delete
|
help: Domain(s) to delete
|
||||||
nargs: "+"
|
nargs: "+"
|
||||||
extra:
|
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()
|
### domain_info()
|
||||||
info:
|
info:
|
||||||
|
@ -216,7 +224,9 @@ domain:
|
||||||
domain:
|
domain:
|
||||||
help: ""
|
help: ""
|
||||||
extra:
|
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
|
help: Name of the list to remove
|
||||||
extra:
|
extra:
|
||||||
ask: "List to remove"
|
ask: "List to remove"
|
||||||
pattern: '^[a-z0-9_]+$'
|
pattern:
|
||||||
|
- '^[a-z0-9_]+$'
|
||||||
|
- "Must be alphanumeric and underscore characters only"
|
||||||
|
|
||||||
### app_list()
|
### app_list()
|
||||||
list:
|
list:
|
||||||
|
@ -302,7 +314,9 @@ app:
|
||||||
full: --user
|
full: --user
|
||||||
help: Allowed app map for a user
|
help: Allowed app map for a user
|
||||||
extra:
|
extra:
|
||||||
pattern: '^[a-z0-9_]+$'
|
pattern:
|
||||||
|
- '^[a-z0-9_]+$'
|
||||||
|
- "Must be alphanumeric and underscore characters only"
|
||||||
|
|
||||||
|
|
||||||
### app_install() TODO: Write help
|
### app_install() TODO: Write help
|
||||||
|
@ -388,7 +402,9 @@ app:
|
||||||
port:
|
port:
|
||||||
help: Port to check
|
help: Port to check
|
||||||
extra:
|
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()
|
### app_checkurl()
|
||||||
checkurl:
|
checkurl:
|
||||||
|
@ -659,7 +675,9 @@ service:
|
||||||
help: Number of lines to display
|
help: Number of lines to display
|
||||||
default: "50"
|
default: "50"
|
||||||
extra:
|
extra:
|
||||||
pattern: '^[0-9]+$'
|
pattern:
|
||||||
|
- '^[0-9]+$'
|
||||||
|
- "Must be a valid number"
|
||||||
|
|
||||||
|
|
||||||
#############################
|
#############################
|
||||||
|
@ -691,7 +709,9 @@ firewall:
|
||||||
port:
|
port:
|
||||||
help: Port to open
|
help: Port to open
|
||||||
extra:
|
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:
|
protocol:
|
||||||
help: Protocol associated with port
|
help: Protocol associated with port
|
||||||
choices:
|
choices:
|
||||||
|
@ -840,12 +860,16 @@ tools:
|
||||||
-o:
|
-o:
|
||||||
full: --old-domain
|
full: --old-domain
|
||||||
extra:
|
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:
|
-n:
|
||||||
full: --new-domain
|
full: --new-domain
|
||||||
extra:
|
extra:
|
||||||
ask: "New main domain"
|
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()
|
### tools_postinstall()
|
||||||
postinstall:
|
postinstall:
|
||||||
|
@ -857,7 +881,9 @@ tools:
|
||||||
help: YunoHost main domain
|
help: YunoHost main domain
|
||||||
extra:
|
extra:
|
||||||
ask: "Main domain"
|
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:
|
-p:
|
||||||
full: --password
|
full: --password
|
||||||
help: YunoHost admin password
|
help: YunoHost admin password
|
||||||
|
|
|
@ -24,68 +24,98 @@ __credits__ = """
|
||||||
You should have received a copy of the GNU Affero General Public License
|
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
|
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):
|
def api(port, routes={}, use_cache=True):
|
||||||
"""
|
"""Web server (API) interface
|
||||||
|
|
||||||
Run a HTTP server with the moulinette for an API usage.
|
Run a HTTP server with the moulinette for an API usage.
|
||||||
|
|
||||||
Keyword arguments:
|
Keyword arguments:
|
||||||
|
|
||||||
- port -- Port to run on
|
- port -- Port to run on
|
||||||
|
|
||||||
- routes -- A dict of additional routes to add in the form of
|
- routes -- A dict of additional routes to add in the form of
|
||||||
{(method, uri): callback}
|
{(method, uri): callback}
|
||||||
|
|
||||||
- use_cache -- False if it should parse the actions map file
|
- use_cache -- False if it should parse the actions map file
|
||||||
instead of using the cached one
|
instead of using the cached one
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from bottle import run
|
from bottle import run
|
||||||
from core.actionsmap import ActionsMap
|
from .actionsmap import ActionsMap
|
||||||
from core.api import MoulinetteAPI
|
from .interface.api import MoulinetteAPI
|
||||||
from core.helpers import Interface
|
|
||||||
|
|
||||||
amap = ActionsMap(Interface.api, use_cache=use_cache)
|
amap = ActionsMap('api', use_cache=use_cache)
|
||||||
moulinette = MoulinetteAPI(amap, routes)
|
moulinette = MoulinetteAPI(amap, routes)
|
||||||
|
|
||||||
run(moulinette.app, port=port)
|
run(moulinette.app, port=port)
|
||||||
|
|
||||||
def cli(args, use_cache=True):
|
def cli(args, use_cache=True):
|
||||||
"""
|
"""Command line interface
|
||||||
|
|
||||||
Execute an action with the moulinette from the CLI and print its
|
Execute an action with the moulinette from the CLI and print its
|
||||||
result in a readable format.
|
result in a readable format.
|
||||||
|
|
||||||
Keyword arguments:
|
Keyword arguments:
|
||||||
|
|
||||||
- args -- A list of argument strings
|
- args -- A list of argument strings
|
||||||
|
|
||||||
- use_cache -- False if it should parse the actions map file
|
- use_cache -- False if it should parse the actions map file
|
||||||
instead of using the cached one
|
instead of using the cached one
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
from core.actionsmap import ActionsMap
|
from .actionsmap import ActionsMap
|
||||||
from core.helpers import Interface, YunoHostError, pretty_print_dict
|
from .helpers import YunoHostError, pretty_print_dict
|
||||||
|
|
||||||
lock_file = '/var/run/moulinette.lock'
|
lock_file = '/var/run/moulinette.lock'
|
||||||
|
|
||||||
# TODO: Move the lock checking into the ActionsMap class
|
# TODO: Move the lock checking into the ActionsMap class
|
||||||
# Check the lock
|
# Check the lock
|
||||||
if os.path.isfile(lock_file):
|
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
|
# Create a lock
|
||||||
with open(lock_file, 'w') as f: pass
|
with open(lock_file, 'w') as f: pass
|
||||||
os.system('chmod 400 '+ lock_file)
|
os.system('chmod 400 '+ lock_file)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
amap = ActionsMap(Interface.cli, use_cache=use_cache)
|
amap = ActionsMap('cli', use_cache=use_cache)
|
||||||
pretty_print_dict(amap.process(args))
|
pretty_print_dict(amap.process(args))
|
||||||
except KeyboardInterrupt, EOFError:
|
except KeyboardInterrupt, EOFError:
|
||||||
raise YunoHostError(125, _("Interrupted"))
|
raise MoulinetteError(125, _("Interrupted"))
|
||||||
finally:
|
finally:
|
||||||
# Remove the lock
|
# Remove the lock
|
||||||
os.remove(lock_file)
|
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 re
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from helpers import Interface, colorize, YunoHostError
|
from .. import MoulinetteError
|
||||||
|
from ..helpers import colorize
|
||||||
|
|
||||||
class _ExtraParameter(object):
|
class _ExtraParameter(object):
|
||||||
"""
|
"""
|
||||||
|
@ -78,7 +79,7 @@ class AskParameter(_ExtraParameter):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
name = 'ask'
|
name = 'ask'
|
||||||
skipped_iface = {Interface.api}
|
skipped_iface = { 'api' }
|
||||||
|
|
||||||
def __call__(self, message, arg_name, arg_value):
|
def __call__(self, message, arg_name, arg_value):
|
||||||
# TODO: Fix asked arguments ordering
|
# TODO: Fix asked arguments ordering
|
||||||
|
@ -119,7 +120,7 @@ class PasswordParameter(AskParameter):
|
||||||
pwd1 = getpass.getpass(colorize(message + ': ', 'cyan'))
|
pwd1 = getpass.getpass(colorize(message + ': ', 'cyan'))
|
||||||
pwd2 = getpass.getpass(colorize('Retype ' + message + ': ', 'cyan'))
|
pwd2 = getpass.getpass(colorize('Retype ' + message + ': ', 'cyan'))
|
||||||
if pwd1 != pwd2:
|
if pwd1 != pwd2:
|
||||||
raise YunoHostError(22, _("Passwords don't match"))
|
raise MoulinetteError(22, _("Passwords don't match"))
|
||||||
return pwd1
|
return pwd1
|
||||||
|
|
||||||
class PatternParameter(_ExtraParameter):
|
class PatternParameter(_ExtraParameter):
|
||||||
|
@ -137,7 +138,7 @@ class PatternParameter(_ExtraParameter):
|
||||||
message = arguments[1]
|
message = arguments[1]
|
||||||
|
|
||||||
if arg_value is not None and not re.match(pattern, arg_value):
|
if arg_value is not None and not re.match(pattern, arg_value):
|
||||||
raise YunoHostError(22, message)
|
raise MoulinetteError(22, message)
|
||||||
return arg_value
|
return arg_value
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
|
@ -21,21 +21,6 @@ import getpass
|
||||||
if not __debug__:
|
if not __debug__:
|
||||||
import traceback
|
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 = []
|
win = []
|
||||||
|
|
||||||
def random_password(length=8):
|
def random_password(length=8):
|
||||||
|
@ -105,33 +90,6 @@ def win_msg(astr):
|
||||||
win.append(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):
|
def validate(pattern, array):
|
||||||
"""
|
"""
|
||||||
Validate attributes with a pattern
|
Validate attributes with a pattern
|
||||||
|
@ -441,115 +399,3 @@ class YunoHostLDAP(Singleton):
|
||||||
else:
|
else:
|
||||||
raise YunoHostError(17, _('Attribute already exists') + ' "' + attr + '=' + value + '"')
|
raise YunoHostError(17, _('Attribute already exists') + ' "' + attr + '=' + value + '"')
|
||||||
return True
|
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 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import re
|
||||||
|
import argparse
|
||||||
import os.path
|
import os.path
|
||||||
from bottle import Bottle, request, response, HTTPResponse
|
from bottle import Bottle, request, response, HTTPResponse
|
||||||
from beaker.middleware import SessionMiddleware
|
from beaker.middleware import SessionMiddleware
|
||||||
|
|
||||||
from ..config import session_path, doc_json_path
|
from . import BaseParser
|
||||||
from helpers import YunoHostError, YunoHostLDAP
|
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.
|
Manage the authentication for the API access.
|
||||||
|
|
||||||
|
@ -20,8 +166,7 @@ class APIAuthPlugin(object):
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
# TODO: Add options (e.g. session type, content type, ...)
|
# TODO: Add options (e.g. session type, content type, ...)
|
||||||
if not os.path.isdir(session_path):
|
pass
|
||||||
os.makedirs(session_path)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def app(self):
|
def app(self):
|
||||||
|
@ -44,7 +189,7 @@ class APIAuthPlugin(object):
|
||||||
session_opts = {
|
session_opts = {
|
||||||
'session.type': 'file',
|
'session.type': 'file',
|
||||||
'session.cookie_expires': True,
|
'session.cookie_expires': True,
|
||||||
'session.data_dir': session_path,
|
'session.data_dir': pkg.cachedir('session', make_dir=True),
|
||||||
'session.secure': True
|
'session.secure': True
|
||||||
}
|
}
|
||||||
self._app = SessionMiddleware(app, session_opts)
|
self._app = SessionMiddleware(app, session_opts)
|
||||||
|
@ -119,7 +264,7 @@ class APIAuthPlugin(object):
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
class ActionsMapPlugin(object):
|
class _ActionsMapPlugin(object):
|
||||||
"""
|
"""
|
||||||
Process action for the request using the actions map.
|
Process action for the request using the actions map.
|
||||||
|
|
||||||
|
@ -158,19 +303,15 @@ class ActionsMapPlugin(object):
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|
||||||
## Main class
|
|
||||||
|
|
||||||
class MoulinetteAPI(object):
|
class MoulinetteAPI(object):
|
||||||
"""
|
"""
|
||||||
Initialize a HTTP server which serves the API to access to the
|
Initialize a HTTP server which serves the API to access to the
|
||||||
moulinette actions.
|
moulinette actions.
|
||||||
|
|
||||||
Keyword arguments:
|
Keyword arguments:
|
||||||
|
|
||||||
- actionsmap -- The relevant ActionsMap instance
|
- actionsmap -- The relevant ActionsMap instance
|
||||||
|
|
||||||
- routes -- A dict of additional routes to add in the form of
|
- routes -- A dict of additional routes to add in the form of
|
||||||
{(method, uri): callback}
|
{(method, path): callback}
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@ -182,14 +323,14 @@ class MoulinetteAPI(object):
|
||||||
callback=self.doc, skip=['apiauth'])
|
callback=self.doc, skip=['apiauth'])
|
||||||
|
|
||||||
# Append routes from the actions map
|
# Append routes from the actions map
|
||||||
amap = ActionsMapPlugin(actionsmap)
|
amap = _ActionsMapPlugin(actionsmap)
|
||||||
for (m, u) in actionsmap.parser.routes:
|
for (m, p) in actionsmap.parser.routes:
|
||||||
app.route(u, method=m, callback=self._error, apply=amap)
|
app.route(p, method=m, callback=self._error, apply=amap)
|
||||||
|
|
||||||
# Append additional routes
|
# Append additional routes
|
||||||
# TODO: Add an option to skip auth for the route
|
# TODO: Add an option to skip auth for the route
|
||||||
for (m, u), c in routes.items():
|
for (m, p), c in routes.items():
|
||||||
app.route(u, method=m, callback=c)
|
app.route(p, method=m, callback=c)
|
||||||
|
|
||||||
# Define and install a plugin which sets proper header
|
# Define and install a plugin which sets proper header
|
||||||
def apiheader(callback):
|
def apiheader(callback):
|
||||||
|
@ -201,7 +342,7 @@ class MoulinetteAPI(object):
|
||||||
app.install(apiheader)
|
app.install(apiheader)
|
||||||
|
|
||||||
# Install authentication plugin
|
# Install authentication plugin
|
||||||
apiauth = APIAuthPlugin()
|
apiauth = _APIAuthPlugin()
|
||||||
app.install(apiauth)
|
app.install(apiauth)
|
||||||
|
|
||||||
self._app = apiauth.app
|
self._app = apiauth.app
|
||||||
|
@ -220,11 +361,11 @@ class MoulinetteAPI(object):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if category is None:
|
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()
|
return f.read()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with open(doc_json_path +'/'+ category +'.json') as f:
|
with open(pkg.datafile('doc/%s.json' % category)) as f:
|
||||||
return f.read()
|
return f.read()
|
||||||
except IOError:
|
except IOError:
|
||||||
return 'unknown'
|
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 user import user_info, user_list
|
||||||
from hook import hook_exec, hook_add, hook_remove
|
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'
|
repo_path = '/var/cache/yunohost/repo'
|
||||||
apps_path = '/usr/share/yunohost/apps'
|
apps_path = '/usr/share/yunohost/apps'
|
||||||
|
|
|
@ -29,7 +29,7 @@ import json
|
||||||
import yaml
|
import yaml
|
||||||
import glob
|
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):
|
def backup_init(helper=False):
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -34,7 +34,7 @@ import requests
|
||||||
from urllib import urlopen
|
from urllib import urlopen
|
||||||
from dyndns import dyndns_subscribe
|
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):
|
def domain_list(filter=None, limit=None, offset=None):
|
||||||
|
|
|
@ -30,7 +30,7 @@ import json
|
||||||
import glob
|
import glob
|
||||||
import base64
|
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):
|
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.stderr.write('apt-get install python-yaml\n')
|
||||||
sys.exit(1)
|
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):
|
def firewall_allow(protocol=None, port=None, ipv6=None, upnp=False):
|
||||||
|
|
|
@ -28,7 +28,7 @@ import sys
|
||||||
import re
|
import re
|
||||||
import json
|
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/'
|
hook_folder = '/usr/share/yunohost/hooks/'
|
||||||
|
|
||||||
|
|
|
@ -37,7 +37,7 @@ from datetime import datetime, timedelta
|
||||||
from service import (service_enable, service_disable,
|
from service import (service_enable, service_disable,
|
||||||
service_start, service_stop, service_status)
|
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'
|
glances_uri = 'http://127.0.0.1:61209'
|
||||||
stats_path = '/var/lib/yunohost/stats'
|
stats_path = '/var/lib/yunohost/stats'
|
||||||
|
|
|
@ -28,7 +28,7 @@ import glob
|
||||||
import subprocess
|
import subprocess
|
||||||
import os.path
|
import os.path
|
||||||
|
|
||||||
from moulinette.core.helpers import YunoHostError, win_msg
|
from moulinette.helpers import YunoHostError, win_msg
|
||||||
|
|
||||||
|
|
||||||
def service_start(names):
|
def service_start(names):
|
||||||
|
|
|
@ -36,7 +36,7 @@ from dyndns import dyndns_subscribe
|
||||||
from backup import backup_init
|
from backup import backup_init
|
||||||
from app import app_ssowatconf
|
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):
|
def tools_ldapinit(password=None):
|
||||||
|
|
|
@ -33,7 +33,7 @@ import getpass
|
||||||
from domain import domain_list
|
from domain import domain_list
|
||||||
from hook import hook_callback
|
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):
|
def user_list(fields=None, filter=None, limit=None, offset=None):
|
||||||
"""
|
"""
|
||||||
|
|
Loading…
Add table
Reference in a new issue