mirror of
https://github.com/YunoHost/moulinette.git
synced 2024-09-03 20:06:31 +02:00
Standardize extra parameters and make them modular
This commit is contained in:
parent
a2be4d6e12
commit
0bfc63e4b8
5 changed files with 374 additions and 168 deletions
|
@ -74,23 +74,29 @@ user:
|
||||||
-u:
|
-u:
|
||||||
full: --username
|
full: --username
|
||||||
help: Must be unique
|
help: Must be unique
|
||||||
ask: "Username"
|
extra:
|
||||||
pattern: '^[a-z0-9_]+$'
|
ask: "Username"
|
||||||
|
pattern: '^[a-z0-9_]+$'
|
||||||
-f:
|
-f:
|
||||||
full: --firstname
|
full: --firstname
|
||||||
ask: "Firstname"
|
extra:
|
||||||
|
ask: "Firstname"
|
||||||
-l:
|
-l:
|
||||||
full: --lastname
|
full: --lastname
|
||||||
ask: "Lastname"
|
extra:
|
||||||
|
ask: "Lastname"
|
||||||
-m:
|
-m:
|
||||||
full: --mail
|
full: --mail
|
||||||
help: Main mail address must be unique
|
help: Main mail address must be unique
|
||||||
ask: "Mail address"
|
extra:
|
||||||
pattern: '^[\w.-]+@[\w.-]+\.[a-zA-Z]{2,6}$'
|
ask: "Mail address"
|
||||||
|
pattern:
|
||||||
|
- '^[\w.-]+@[\w.-]+\.[a-zA-Z]{2,6}$'
|
||||||
|
- "Must be a valid email address (e.g. someone@domain.org)"
|
||||||
-p:
|
-p:
|
||||||
full: --password
|
full: --password
|
||||||
ask: "User password"
|
extra:
|
||||||
password: yes
|
password: "User password"
|
||||||
|
|
||||||
### user_delete()
|
### user_delete()
|
||||||
delete:
|
delete:
|
||||||
|
@ -100,9 +106,10 @@ user:
|
||||||
-u:
|
-u:
|
||||||
full: --users
|
full: --users
|
||||||
help: Username of users to delete
|
help: Username of users to delete
|
||||||
ask: "Users to delete"
|
|
||||||
pattern: '^[a-z0-9_]+$'
|
|
||||||
nargs: "*"
|
nargs: "*"
|
||||||
|
extra:
|
||||||
|
ask: "Users to delete"
|
||||||
|
pattern: '^[a-z0-9_]+$'
|
||||||
--purge:
|
--purge:
|
||||||
action: store_true
|
action: store_true
|
||||||
|
|
||||||
|
@ -179,7 +186,8 @@ domain:
|
||||||
domains:
|
domains:
|
||||||
help: Domain name to add
|
help: Domain name to add
|
||||||
nargs: '+'
|
nargs: '+'
|
||||||
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])*)$'
|
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])*)$'
|
||||||
-m:
|
-m:
|
||||||
full: --main
|
full: --main
|
||||||
help: Is the main domain
|
help: Is the main domain
|
||||||
|
@ -197,7 +205,8 @@ domain:
|
||||||
domains:
|
domains:
|
||||||
help: Domain(s) to delete
|
help: Domain(s) to delete
|
||||||
nargs: "+"
|
nargs: "+"
|
||||||
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])*)$'
|
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])*)$'
|
||||||
|
|
||||||
### domain_info()
|
### domain_info()
|
||||||
info:
|
info:
|
||||||
|
@ -206,7 +215,8 @@ domain:
|
||||||
arguments:
|
arguments:
|
||||||
domain:
|
domain:
|
||||||
help: ""
|
help: ""
|
||||||
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])*)$'
|
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])*)$'
|
||||||
|
|
||||||
|
|
||||||
#############################
|
#############################
|
||||||
|
@ -241,8 +251,9 @@ app:
|
||||||
-n:
|
-n:
|
||||||
full: --name
|
full: --name
|
||||||
help: Name of the list to remove
|
help: Name of the list to remove
|
||||||
ask: "List to remove"
|
extra:
|
||||||
pattern: '^[a-z0-9_]+$'
|
ask: "List to remove"
|
||||||
|
pattern: '^[a-z0-9_]+$'
|
||||||
|
|
||||||
### app_list()
|
### app_list()
|
||||||
list:
|
list:
|
||||||
|
@ -290,7 +301,8 @@ app:
|
||||||
-u:
|
-u:
|
||||||
full: --user
|
full: --user
|
||||||
help: Allowed app map for a user
|
help: Allowed app map for a user
|
||||||
pattern: '^[a-z0-9_]+$'
|
extra:
|
||||||
|
pattern: '^[a-z0-9_]+$'
|
||||||
|
|
||||||
|
|
||||||
### app_install() TODO: Write help
|
### app_install() TODO: Write help
|
||||||
|
@ -375,7 +387,8 @@ app:
|
||||||
arguments:
|
arguments:
|
||||||
port:
|
port:
|
||||||
help: Port to check
|
help: Port to check
|
||||||
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])$'
|
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])$'
|
||||||
|
|
||||||
### app_checkurl()
|
### app_checkurl()
|
||||||
checkurl:
|
checkurl:
|
||||||
|
@ -644,8 +657,9 @@ service:
|
||||||
-n:
|
-n:
|
||||||
full: --number
|
full: --number
|
||||||
help: Number of lines to display
|
help: Number of lines to display
|
||||||
pattern: '^[0-9]+$'
|
|
||||||
default: "50"
|
default: "50"
|
||||||
|
extra:
|
||||||
|
pattern: '^[0-9]+$'
|
||||||
|
|
||||||
|
|
||||||
#############################
|
#############################
|
||||||
|
@ -676,7 +690,8 @@ firewall:
|
||||||
arguments:
|
arguments:
|
||||||
port:
|
port:
|
||||||
help: Port to open
|
help: Port to open
|
||||||
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])$'
|
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])$'
|
||||||
protocol:
|
protocol:
|
||||||
help: Protocol associated with port
|
help: Protocol associated with port
|
||||||
choices:
|
choices:
|
||||||
|
@ -810,12 +825,12 @@ tools:
|
||||||
arguments:
|
arguments:
|
||||||
-o:
|
-o:
|
||||||
full: --old-password
|
full: --old-password
|
||||||
ask: "Current admin password"
|
extra:
|
||||||
password: yes
|
password: "Current admin password"
|
||||||
-n:
|
-n:
|
||||||
full: --new-password
|
full: --new-password
|
||||||
ask: "New admin password"
|
extra:
|
||||||
password: yes
|
password: "New admin password"
|
||||||
|
|
||||||
### tools_maindomain()
|
### tools_maindomain()
|
||||||
maindomain:
|
maindomain:
|
||||||
|
@ -824,11 +839,13 @@ tools:
|
||||||
arguments:
|
arguments:
|
||||||
-o:
|
-o:
|
||||||
full: --old-domain
|
full: --old-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])*)$'
|
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])*)$'
|
||||||
-n:
|
-n:
|
||||||
full: --new-domain
|
full: --new-domain
|
||||||
ask: "New main 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])*)$'
|
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])*)$'
|
||||||
|
|
||||||
### tools_postinstall()
|
### tools_postinstall()
|
||||||
postinstall:
|
postinstall:
|
||||||
|
@ -838,13 +855,14 @@ tools:
|
||||||
-d:
|
-d:
|
||||||
full: --domain
|
full: --domain
|
||||||
help: YunoHost main domain
|
help: YunoHost main domain
|
||||||
ask: "Main 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])*)$'
|
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])*)$'
|
||||||
-p:
|
-p:
|
||||||
full: --password
|
full: --password
|
||||||
help: YunoHost admin password
|
help: YunoHost admin password
|
||||||
ask: "New admin password"
|
extra:
|
||||||
password: yes
|
password: "New admin password"
|
||||||
--dyndns:
|
--dyndns:
|
||||||
help: Subscribe domain to a DynDNS service
|
help: Subscribe domain to a DynDNS service
|
||||||
action: store_true
|
action: store_true
|
||||||
|
|
|
@ -46,8 +46,9 @@ def api(port, routes={}, use_cache=True):
|
||||||
from bottle import run
|
from bottle import run
|
||||||
from core.actionsmap import ActionsMap
|
from core.actionsmap import ActionsMap
|
||||||
from core.api import MoulinetteAPI
|
from core.api import MoulinetteAPI
|
||||||
|
from core.helpers import Interface
|
||||||
|
|
||||||
amap = ActionsMap(ActionsMap.IFACE_API, use_cache=use_cache)
|
amap = ActionsMap(Interface.api, use_cache=use_cache)
|
||||||
moulinette = MoulinetteAPI(amap, routes)
|
moulinette = MoulinetteAPI(amap, routes)
|
||||||
|
|
||||||
run(moulinette.app, port=port)
|
run(moulinette.app, port=port)
|
||||||
|
@ -67,10 +68,11 @@ def cli(args, use_cache=True):
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
from core.actionsmap import ActionsMap
|
from core.actionsmap import ActionsMap
|
||||||
from core.helpers import YunoHostError, pretty_print_dict
|
from core.helpers import Interface, 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
|
||||||
# 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 YunoHostError(1, _("The moulinette is already running"))
|
||||||
|
@ -80,7 +82,7 @@ def cli(args, use_cache=True):
|
||||||
os.system('chmod 400 '+ lock_file)
|
os.system('chmod 400 '+ lock_file)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
amap = ActionsMap(ActionsMap.IFACE_CLI, use_cache=use_cache)
|
amap = ActionsMap(Interface.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 YunoHostError(125, _("Interrupted"))
|
||||||
|
|
|
@ -1,16 +1,21 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import getpass
|
|
||||||
import marshal
|
|
||||||
import pickle
|
import pickle
|
||||||
import yaml
|
import yaml
|
||||||
import re
|
import re
|
||||||
import os
|
import os
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
from .. import __version__
|
from .. import __version__
|
||||||
from ..config import actionsmap_path, actionsmap_cache_path
|
from ..config import actionsmap_path, actionsmap_cache_path
|
||||||
from helpers import YunoHostError, colorize
|
|
||||||
|
from extraparameters import extraparameters_list
|
||||||
|
from helpers import Interface, YunoHostError
|
||||||
|
|
||||||
|
## Additional parsers
|
||||||
|
|
||||||
class _HTTPArgumentParser(object):
|
class _HTTPArgumentParser(object):
|
||||||
|
|
||||||
|
@ -137,80 +142,75 @@ class HTTPParser(object):
|
||||||
|
|
||||||
return self._parsers[key].parse_args(args)
|
return self._parsers[key].parse_args(args)
|
||||||
|
|
||||||
|
class ExtraParser(object):
|
||||||
|
"""
|
||||||
|
Global parser for the extra parameters.
|
||||||
|
|
||||||
class _ExtraParameters(object):
|
"""
|
||||||
|
def __init__(self, iface):
|
||||||
|
self.iface = iface
|
||||||
|
self.extra = OrderedDict()
|
||||||
|
|
||||||
CLI_PARAMETERS = ['ask', 'password', 'pattern']
|
# Append available extra parameters for the current interface
|
||||||
API_PARAMETERS = ['pattern']
|
for klass in extraparameters_list:
|
||||||
AVAILABLE_PARAMETERS = CLI_PARAMETERS
|
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 __init__(self, **kwargs):
|
def validate(self, arg_name, parameters):
|
||||||
self._params = {}
|
"""
|
||||||
|
Validate values of extra parameters for an argument
|
||||||
|
|
||||||
for k, v in kwargs.items():
|
Keyword arguments:
|
||||||
if k in self.AVAILABLE_PARAMETERS:
|
- arg_name -- The argument name
|
||||||
self._params[k] = v
|
- parameters -- A dict of extra parameters with their values
|
||||||
|
|
||||||
def validate(self, p_name, p_value):
|
"""
|
||||||
ret = type(p_value)() if p_value is not None else None
|
# Iterate over parameters to validate
|
||||||
|
for p, v in parameters.items():
|
||||||
|
# Remove unknow parameters
|
||||||
|
if p not in self.extra.keys():
|
||||||
|
del parameters[p]
|
||||||
|
|
||||||
for p, v in self._params.items():
|
# Validate parameter value
|
||||||
func = getattr(self, 'process_' + p)
|
parameters[p] = self.extra[p].validate(v, arg_name)
|
||||||
|
|
||||||
if isinstance(ret, list):
|
return parameters
|
||||||
for p_v in p_value:
|
|
||||||
r = func(v, p_name, p_v)
|
def parse(self, arg_name, arg_value, parameters):
|
||||||
if r is not None:
|
"""
|
||||||
ret.append(r)
|
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:
|
else:
|
||||||
r = func(v, p_name, p_value)
|
arg_value = parser(parameters[p], arg_name, arg_value)
|
||||||
if r is not None:
|
|
||||||
ret = r
|
|
||||||
|
|
||||||
return ret
|
return arg_value
|
||||||
|
|
||||||
|
|
||||||
## Parameters validating's method
|
## Main class
|
||||||
# TODO: Add doc
|
|
||||||
|
|
||||||
def process_ask(self, message, p_name, p_value):
|
|
||||||
# TODO: Fix asked arguments ordering
|
|
||||||
if not self._can_prompt(p_value):
|
|
||||||
return p_value
|
|
||||||
|
|
||||||
# Skip password asking
|
|
||||||
if 'password' in self._params.keys():
|
|
||||||
return None
|
|
||||||
|
|
||||||
ret = raw_input(colorize(message + ': ', 'cyan'))
|
|
||||||
return ret
|
|
||||||
|
|
||||||
def process_password(self, is_password, p_name, p_value):
|
|
||||||
if not self._can_prompt(p_value):
|
|
||||||
return p_value
|
|
||||||
|
|
||||||
message = self._params['ask']
|
|
||||||
pwd1 = getpass.getpass(colorize(message + ': ', 'cyan'))
|
|
||||||
pwd2 = getpass.getpass(colorize('Retype ' + message + ': ', 'cyan'))
|
|
||||||
if pwd1 != pwd2:
|
|
||||||
raise YunoHostError(22, _("Passwords don't match"))
|
|
||||||
return pwd1
|
|
||||||
|
|
||||||
def process_pattern(self, pattern, p_name, p_value):
|
|
||||||
# TODO: Add a pattern_help parameter
|
|
||||||
# TODO: Fix missing pattern matching on asking
|
|
||||||
if p_value is not None and not re.match(pattern, p_value):
|
|
||||||
raise YunoHostError(22, _("'%s' argument not match pattern" % p_name))
|
|
||||||
return p_value
|
|
||||||
|
|
||||||
|
|
||||||
## Private method
|
|
||||||
|
|
||||||
def _can_prompt(self, p_value):
|
|
||||||
if os.isatty(1) and (p_value is None or p_value == ''):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
class ActionsMap(object):
|
class ActionsMap(object):
|
||||||
"""
|
"""
|
||||||
|
@ -231,30 +231,37 @@ class ActionsMap(object):
|
||||||
instead of using the cached one.
|
instead of using the cached one.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
IFACE_CLI = 'cli'
|
|
||||||
IFACE_API = 'api'
|
|
||||||
|
|
||||||
def __init__(self, interface, use_cache=True):
|
def __init__(self, interface, use_cache=True):
|
||||||
if interface not in [self.IFACE_CLI,self.IFACE_API]:
|
if interface not in Interface.all():
|
||||||
raise ValueError(_("Invalid interface '%s'" % interface))
|
raise ValueError(_("Invalid interface '%s'" % interface))
|
||||||
self.interface = interface
|
self.interface = interface
|
||||||
|
self.use_cache = use_cache
|
||||||
|
|
||||||
|
logging.debug("initializing ActionsMap for the '%s' interface" % interface)
|
||||||
|
|
||||||
# Iterate over actions map namespaces
|
# Iterate over actions map namespaces
|
||||||
actionsmap = {}
|
actionsmaps = {}
|
||||||
for n in self.get_actionsmap_namespaces():
|
for n in self.get_actionsmap_namespaces():
|
||||||
|
logging.debug("loading '%s' actions map namespace" % n)
|
||||||
|
|
||||||
if use_cache:
|
if use_cache:
|
||||||
|
# Attempt to load cache if it exists
|
||||||
cache_file = '%s/%s.pkl' % (actionsmap_cache_path, n)
|
cache_file = '%s/%s.pkl' % (actionsmap_cache_path, n)
|
||||||
if os.path.isfile(cache_file):
|
if os.path.isfile(cache_file):
|
||||||
with open(cache_file, 'r') as f:
|
with open(cache_file, 'r') as f:
|
||||||
actionsmap[n] = pickle.load(f)
|
actionsmaps[n] = pickle.load(f)
|
||||||
else:
|
else:
|
||||||
actionsmap = self.generate_cache()
|
self.use_cache = False
|
||||||
|
actionsmaps = self.generate_cache()
|
||||||
|
break
|
||||||
else:
|
else:
|
||||||
am_file = '%s/%s.yml' % (actionsmap_path, n)
|
am_file = '%s/%s.yml' % (actionsmap_path, n)
|
||||||
with open(am_file, 'r') as f:
|
with open(am_file, 'r') as f:
|
||||||
actionsmap[n] = yaml.load(f)
|
actionsmaps[n] = yaml.load(f)
|
||||||
|
|
||||||
self.parser = self._construct_parser(actionsmap)
|
# Generate parsers
|
||||||
|
self.extraparser = ExtraParser(interface)
|
||||||
|
self.parser = self._construct_parser(actionsmaps)
|
||||||
|
|
||||||
def process(self, args, route=None):
|
def process(self, args, route=None):
|
||||||
"""
|
"""
|
||||||
|
@ -268,9 +275,9 @@ class ActionsMap(object):
|
||||||
arguments = None
|
arguments = None
|
||||||
|
|
||||||
# Parse arguments
|
# Parse arguments
|
||||||
if self.interface ==self.IFACE_CLI:
|
if self.interface == Interface.cli:
|
||||||
arguments = self.parser.parse_args(args)
|
arguments = self.parser.parse_args(args)
|
||||||
elif self.interface ==self.IFACE_API:
|
elif self.interface == Interface.api:
|
||||||
if route is None:
|
if route is None:
|
||||||
# TODO: Raise a proper exception
|
# TODO: Raise a proper exception
|
||||||
raise Exception(_("Missing route argument"))
|
raise Exception(_("Missing route argument"))
|
||||||
|
@ -302,7 +309,10 @@ class ActionsMap(object):
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_actionsmap_namespaces(path=actionsmap_path):
|
def get_actionsmap_namespaces(path=actionsmap_path):
|
||||||
"""
|
"""
|
||||||
Retrieve actions map namespaces in a given path
|
Retrieve actions map namespaces from a given path
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A list of available namespaces
|
||||||
|
|
||||||
"""
|
"""
|
||||||
namespaces = []
|
namespaces = []
|
||||||
|
@ -313,65 +323,77 @@ class ActionsMap(object):
|
||||||
return namespaces
|
return namespaces
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def generate_cache(cls):
|
def generate_cache(klass):
|
||||||
"""
|
"""
|
||||||
Generate cache for the actions map's file(s)
|
Generate cache for the actions map's file(s)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
A dict of actions map for each namespaces
|
||||||
|
|
||||||
"""
|
"""
|
||||||
actionsmap = {}
|
actionsmaps = {}
|
||||||
|
|
||||||
if not os.path.isdir(actionsmap_cache_path):
|
if not os.path.isdir(actionsmap_cache_path):
|
||||||
os.makedirs(actionsmap_cache_path)
|
os.makedirs(actionsmap_cache_path)
|
||||||
|
|
||||||
for n in cls.get_actionsmap_namespaces():
|
# 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)
|
am_file = '%s/%s.yml' % (actionsmap_path, n)
|
||||||
with open(am_file, 'r') as f:
|
with open(am_file, 'r') as f:
|
||||||
actionsmap[n] = yaml.load(f)
|
actionsmaps[n] = yaml.load(f)
|
||||||
|
|
||||||
|
# Cache actions map into pickle file
|
||||||
cache_file = '%s/%s.pkl' % (actionsmap_cache_path, n)
|
cache_file = '%s/%s.pkl' % (actionsmap_cache_path, n)
|
||||||
with open(cache_file, 'w') as f:
|
with open(cache_file, 'w') as f:
|
||||||
pickle.dump(actionsmap[n], f)
|
pickle.dump(actionsmaps[n], f)
|
||||||
|
|
||||||
return actionsmap
|
return actionsmaps
|
||||||
|
|
||||||
|
|
||||||
## Private class and methods
|
## Private class and methods
|
||||||
|
|
||||||
def _store_extra_parameters(self, parser, arg_name, arg_params):
|
def _store_extra_parameters(self, parser, arg_name, arg_params):
|
||||||
"""
|
"""
|
||||||
Store extra parameters for a given parser's argument name
|
Store extra parameters for a given argument
|
||||||
|
|
||||||
Keyword arguments:
|
Keyword arguments:
|
||||||
- parser -- Parser object of the argument
|
- parser -- Parser object for the arguments
|
||||||
- arg_name -- Argument name
|
- arg_name -- Argument name
|
||||||
- arg_params -- Argument parameters
|
- arg_params -- Argument parameters
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The parser object
|
||||||
|
|
||||||
"""
|
"""
|
||||||
params = {}
|
if 'extra' in arg_params:
|
||||||
keys = []
|
# Retrieve current extra parameters dict
|
||||||
|
|
||||||
# Get available parameters for the current interface
|
|
||||||
if self.interface ==self.IFACE_CLI:
|
|
||||||
keys = _ExtraParameters.CLI_PARAMETERS
|
|
||||||
elif self.interface ==self.IFACE_API:
|
|
||||||
keys = _ExtraParameters.API_PARAMETERS
|
|
||||||
|
|
||||||
for k in keys:
|
|
||||||
if k in arg_params:
|
|
||||||
params[k] = arg_params[k]
|
|
||||||
|
|
||||||
if len(params) > 0:
|
|
||||||
# Retrieve all extra parameters from the parser
|
|
||||||
extra = parser.get_default('_extra')
|
extra = parser.get_default('_extra')
|
||||||
if not extra or not isinstance(extra, dict):
|
if not extra or not isinstance(extra, dict):
|
||||||
extra = {}
|
extra = {}
|
||||||
|
|
||||||
# Add completed extra parameters to the parser
|
if not self.use_cache:
|
||||||
extra[arg_name] = _ExtraParameters(**params)
|
# 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)
|
parser.set_defaults(_extra=extra)
|
||||||
|
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
def _parse_extra_parameters(self, args):
|
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
|
# Retrieve extra parameters from the arguments
|
||||||
if '_extra' not in args:
|
if '_extra' not in args:
|
||||||
return args
|
return args
|
||||||
|
@ -379,41 +401,41 @@ class ActionsMap(object):
|
||||||
del args['_extra']
|
del args['_extra']
|
||||||
|
|
||||||
# Validate extra parameters for each arguments
|
# Validate extra parameters for each arguments
|
||||||
for n, e in extra.items():
|
for an, parameters in extra.items():
|
||||||
args[n] = e.validate(n, args[n])
|
args[an] = self.extraparser.parse(an, args[an], parameters)
|
||||||
|
|
||||||
return args
|
return args
|
||||||
|
|
||||||
def _construct_parser(self, actionsmap):
|
def _construct_parser(self, actionsmaps):
|
||||||
"""
|
"""
|
||||||
Construct the parser with the actions map
|
Construct the parser with the actions map
|
||||||
|
|
||||||
Keyword arguments:
|
Keyword arguments:
|
||||||
- actionsmap -- Multi-level dictionnary of
|
- actionsmaps -- A dict of multi-level dictionnary of
|
||||||
categories/actions/arguments list
|
categories/actions/arguments list for each namespaces
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Interface relevant's parser object
|
An interface relevant's parser object
|
||||||
|
|
||||||
"""
|
"""
|
||||||
top_parser = None
|
top_parser = None
|
||||||
iface = self.interface
|
iface = self.interface
|
||||||
|
|
||||||
# Create parser object
|
# Create parser object
|
||||||
if iface ==self.IFACE_CLI:
|
if iface == Interface.cli:
|
||||||
# TODO: Add descritpion (from __description__)
|
# TODO: Add descritpion (from __description__?)
|
||||||
top_parser = argparse.ArgumentParser()
|
top_parser = argparse.ArgumentParser()
|
||||||
top_subparsers = top_parser.add_subparsers()
|
top_subparsers = top_parser.add_subparsers()
|
||||||
elif iface ==self.IFACE_API:
|
elif iface == Interface.api:
|
||||||
top_parser = HTTPParser()
|
top_parser = HTTPParser()
|
||||||
|
|
||||||
## Extract option strings from parameters
|
## Format option strings from argument parameters
|
||||||
def _option_strings(arg_name, arg_params):
|
def _option_strings(arg_name, arg_params):
|
||||||
if iface ==self.IFACE_CLI:
|
if iface == Interface.cli:
|
||||||
if arg_name[0] == '-' and 'full' in arg_params:
|
if arg_name[0] == '-' and 'full' in arg_params:
|
||||||
return [arg_name, arg_params['full']]
|
return [arg_name, arg_params['full']]
|
||||||
return [arg_name]
|
return [arg_name]
|
||||||
elif iface ==self.IFACE_API:
|
elif iface == Interface.api:
|
||||||
if arg_name[0] != '-':
|
if arg_name[0] != '-':
|
||||||
return [arg_name]
|
return [arg_name]
|
||||||
if 'full' in arg_params:
|
if 'full' in arg_params:
|
||||||
|
@ -422,40 +444,31 @@ class ActionsMap(object):
|
||||||
return [arg_name.replace('--', '@', 1)]
|
return [arg_name.replace('--', '@', 1)]
|
||||||
return [arg_name.replace('-', '@', 1)]
|
return [arg_name.replace('-', '@', 1)]
|
||||||
|
|
||||||
## Extract a key from parameters
|
|
||||||
def _key(arg_params, key, default=str()):
|
|
||||||
if key in arg_params:
|
|
||||||
return arg_params[key]
|
|
||||||
return default
|
|
||||||
|
|
||||||
## Remove extra parameters
|
## Remove extra parameters
|
||||||
def _clean_params(arg_params):
|
def _clean_params(arg_params):
|
||||||
keys = list(_ExtraParameters.AVAILABLE_PARAMETERS)
|
for k in {'full', 'extra'}:
|
||||||
keys.append('full')
|
|
||||||
|
|
||||||
for k in keys:
|
|
||||||
if k in arg_params:
|
if k in arg_params:
|
||||||
del arg_params[k]
|
del arg_params[k]
|
||||||
return arg_params
|
return arg_params
|
||||||
|
|
||||||
# Iterate over actions map namespaces
|
# Iterate over actions map namespaces
|
||||||
for n in self.get_actionsmap_namespaces():
|
for n, actionsmap in actionsmaps.items():
|
||||||
# Parse general arguments for the cli only
|
# Parse general arguments for the cli only
|
||||||
if iface ==self.IFACE_CLI:
|
if iface == Interface.cli:
|
||||||
for an, ap in actionsmap[n]['general_arguments'].items():
|
for an, ap in actionsmap['general_arguments'].items():
|
||||||
if 'version' in ap:
|
if 'version' in ap:
|
||||||
ap['version'] = ap['version'].replace('%version%', __version__)
|
ap['version'] = ap['version'].replace('%version%', __version__)
|
||||||
top_parser.add_argument(*_option_strings(an, ap), **_clean_params(ap))
|
top_parser.add_argument(*_option_strings(an, ap), **_clean_params(ap))
|
||||||
del actionsmap[n]['general_arguments']
|
del actionsmap['general_arguments']
|
||||||
|
|
||||||
# Parse categories
|
# Parse categories
|
||||||
for cn, cp in actionsmap[n].items():
|
for cn, cp in actionsmap.items():
|
||||||
if 'actions' not in cp:
|
if 'actions' not in cp:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# Add category subparsers for the cli only
|
# Add category subparsers for the cli only
|
||||||
if iface ==self.IFACE_CLI:
|
if iface == Interface.cli:
|
||||||
c_help = _key(cp, 'category_help')
|
c_help = cp.get('category_help')
|
||||||
subparsers = top_subparsers.add_parser(cn, help=c_help).add_subparsers()
|
subparsers = top_subparsers.add_parser(cn, help=c_help).add_subparsers()
|
||||||
|
|
||||||
# Parse actions
|
# Parse actions
|
||||||
|
@ -463,10 +476,10 @@ class ActionsMap(object):
|
||||||
parser = None
|
parser = None
|
||||||
|
|
||||||
# Add parser for the current action
|
# Add parser for the current action
|
||||||
if iface ==self.IFACE_CLI:
|
if iface == Interface.cli:
|
||||||
a_help = _key(ap, 'action_help')
|
a_help = ap.get('action_help')
|
||||||
parser = subparsers.add_parser(an, help=a_help)
|
parser = subparsers.add_parser(an, help=a_help)
|
||||||
elif iface ==self.IFACE_API and 'api' in ap:
|
elif iface == Interface.api and 'api' in ap:
|
||||||
# Extract method and uri
|
# Extract method and uri
|
||||||
m = re.match('(GET|POST|PUT|DELETE) (/\S+)', ap['api'])
|
m = re.match('(GET|POST|PUT|DELETE) (/\S+)', ap['api'])
|
||||||
if m:
|
if m:
|
||||||
|
|
158
src/moulinette/core/extraparameters.py
Normal file
158
src/moulinette/core/extraparameters.py
Normal file
|
@ -0,0 +1,158 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import getpass
|
||||||
|
import re
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from helpers import Interface, colorize, YunoHostError
|
||||||
|
|
||||||
|
class _ExtraParameter(object):
|
||||||
|
"""
|
||||||
|
Argument parser for an extra parameter.
|
||||||
|
|
||||||
|
It is a pure virtual class that each extra parameter classes must
|
||||||
|
implement.
|
||||||
|
|
||||||
|
"""
|
||||||
|
def __init__(self, iface):
|
||||||
|
# TODO: Add conn argument which contains authentification object
|
||||||
|
self.iface = iface
|
||||||
|
|
||||||
|
|
||||||
|
## Required variables
|
||||||
|
# Each extra parameters classes must overwrite these variables.
|
||||||
|
|
||||||
|
"""The extra parameter name"""
|
||||||
|
name = None
|
||||||
|
|
||||||
|
|
||||||
|
## Optional variables
|
||||||
|
# Each extra parameters classes can overwrite these variables.
|
||||||
|
|
||||||
|
"""A list of interface for which the parameter doesn't apply"""
|
||||||
|
skipped_iface = {}
|
||||||
|
|
||||||
|
|
||||||
|
## Virtual methods
|
||||||
|
# Each extra parameters classes can implement these methods.
|
||||||
|
|
||||||
|
def __call__(self, parameter, arg_name, arg_value):
|
||||||
|
"""
|
||||||
|
Parse the argument
|
||||||
|
|
||||||
|
Keyword arguments:
|
||||||
|
- parameter -- The value of this parameter for the action
|
||||||
|
- arg_name -- The argument name
|
||||||
|
- arg_value -- The argument value
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The new argument value
|
||||||
|
|
||||||
|
"""
|
||||||
|
return arg_value
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def validate(value, arg_name):
|
||||||
|
"""
|
||||||
|
Validate the parameter value for an argument
|
||||||
|
|
||||||
|
Keyword arguments:
|
||||||
|
- value -- The parameter value
|
||||||
|
- arg_name -- The argument name
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
The validated parameter value
|
||||||
|
|
||||||
|
"""
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
## Extra parameters definitions
|
||||||
|
|
||||||
|
class AskParameter(_ExtraParameter):
|
||||||
|
"""
|
||||||
|
Ask for the argument value if possible and needed.
|
||||||
|
|
||||||
|
The value of this parameter corresponds to the message to display
|
||||||
|
when asking the argument value.
|
||||||
|
|
||||||
|
"""
|
||||||
|
name = 'ask'
|
||||||
|
skipped_iface = {Interface.api}
|
||||||
|
|
||||||
|
def __call__(self, message, arg_name, arg_value):
|
||||||
|
# TODO: Fix asked arguments ordering
|
||||||
|
if arg_value:
|
||||||
|
return arg_value
|
||||||
|
|
||||||
|
# Ask for the argument value
|
||||||
|
ret = raw_input(colorize(message + ': ', 'cyan'))
|
||||||
|
return ret
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def validate(klass, value, arg_name):
|
||||||
|
# Allow boolean or empty string
|
||||||
|
if isinstance(value, bool) or (isinstance(value, str) and not value):
|
||||||
|
logging.debug("value of '%s' extra parameter for '%s' argument should be a string" \
|
||||||
|
% (klass.name, arg_name))
|
||||||
|
value = arg_name
|
||||||
|
elif not isinstance(value, str):
|
||||||
|
raise TypeError("Invalid type of '%s' extra parameter for '%s' argument" \
|
||||||
|
% (klass.name, arg_name))
|
||||||
|
return value
|
||||||
|
|
||||||
|
class PasswordParameter(AskParameter):
|
||||||
|
"""
|
||||||
|
Ask for the password argument value if possible and needed.
|
||||||
|
|
||||||
|
The value of this parameter corresponds to the message to display
|
||||||
|
when asking the password.
|
||||||
|
|
||||||
|
"""
|
||||||
|
name = 'password'
|
||||||
|
|
||||||
|
def __call__(self, message, arg_name, arg_value):
|
||||||
|
if arg_value:
|
||||||
|
return arg_value
|
||||||
|
|
||||||
|
# Ask for the password
|
||||||
|
pwd1 = getpass.getpass(colorize(message + ': ', 'cyan'))
|
||||||
|
pwd2 = getpass.getpass(colorize('Retype ' + message + ': ', 'cyan'))
|
||||||
|
if pwd1 != pwd2:
|
||||||
|
raise YunoHostError(22, _("Passwords don't match"))
|
||||||
|
return pwd1
|
||||||
|
|
||||||
|
class PatternParameter(_ExtraParameter):
|
||||||
|
"""
|
||||||
|
Check if the argument value match a pattern.
|
||||||
|
|
||||||
|
The value of this parameter corresponds to a list of the pattern and
|
||||||
|
the message to display if it doesn't match.
|
||||||
|
|
||||||
|
"""
|
||||||
|
name = 'pattern'
|
||||||
|
|
||||||
|
def __call__(self, arguments, arg_name, arg_value):
|
||||||
|
pattern = arguments[0]
|
||||||
|
message = arguments[1]
|
||||||
|
|
||||||
|
if arg_value is not None and not re.match(pattern, arg_value):
|
||||||
|
raise YunoHostError(22, message)
|
||||||
|
return arg_value
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def validate(value, arg_name):
|
||||||
|
# Tolerate string type
|
||||||
|
if isinstance(value, str):
|
||||||
|
logging.warning("value of 'pattern' extra parameter for '%s' argument should be a list" % arg_name)
|
||||||
|
value = [value, _("'%s' argument is not matching the pattern") % arg_name]
|
||||||
|
elif not isinstance(value, list) or len(value) != 2:
|
||||||
|
raise TypeError("Invalid type of 'pattern' extra parameter for '%s' argument" % arg_name)
|
||||||
|
return value
|
||||||
|
|
||||||
|
"""
|
||||||
|
The list of available extra parameters classes. It will keep to this list
|
||||||
|
order on argument parsing.
|
||||||
|
|
||||||
|
"""
|
||||||
|
extraparameters_list = {AskParameter, PasswordParameter, PatternParameter}
|
|
@ -21,6 +21,21 @@ 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):
|
||||||
|
|
Loading…
Add table
Reference in a new issue