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:
|
||||
full: --username
|
||||
help: Must be unique
|
||||
ask: "Username"
|
||||
pattern: '^[a-z0-9_]+$'
|
||||
extra:
|
||||
ask: "Username"
|
||||
pattern: '^[a-z0-9_]+$'
|
||||
-f:
|
||||
full: --firstname
|
||||
ask: "Firstname"
|
||||
extra:
|
||||
ask: "Firstname"
|
||||
-l:
|
||||
full: --lastname
|
||||
ask: "Lastname"
|
||||
extra:
|
||||
ask: "Lastname"
|
||||
-m:
|
||||
full: --mail
|
||||
help: Main mail address must be unique
|
||||
ask: "Mail address"
|
||||
pattern: '^[\w.-]+@[\w.-]+\.[a-zA-Z]{2,6}$'
|
||||
extra:
|
||||
ask: "Mail address"
|
||||
pattern:
|
||||
- '^[\w.-]+@[\w.-]+\.[a-zA-Z]{2,6}$'
|
||||
- "Must be a valid email address (e.g. someone@domain.org)"
|
||||
-p:
|
||||
full: --password
|
||||
ask: "User password"
|
||||
password: yes
|
||||
extra:
|
||||
password: "User password"
|
||||
|
||||
### user_delete()
|
||||
delete:
|
||||
|
@ -100,9 +106,10 @@ user:
|
|||
-u:
|
||||
full: --users
|
||||
help: Username of users to delete
|
||||
ask: "Users to delete"
|
||||
pattern: '^[a-z0-9_]+$'
|
||||
nargs: "*"
|
||||
extra:
|
||||
ask: "Users to delete"
|
||||
pattern: '^[a-z0-9_]+$'
|
||||
--purge:
|
||||
action: store_true
|
||||
|
||||
|
@ -179,7 +186,8 @@ domain:
|
|||
domains:
|
||||
help: Domain name to add
|
||||
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:
|
||||
full: --main
|
||||
help: Is the main domain
|
||||
|
@ -197,7 +205,8 @@ domain:
|
|||
domains:
|
||||
help: Domain(s) to delete
|
||||
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()
|
||||
info:
|
||||
|
@ -206,7 +215,8 @@ domain:
|
|||
arguments:
|
||||
domain:
|
||||
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:
|
||||
full: --name
|
||||
help: Name of the list to remove
|
||||
ask: "List to remove"
|
||||
pattern: '^[a-z0-9_]+$'
|
||||
extra:
|
||||
ask: "List to remove"
|
||||
pattern: '^[a-z0-9_]+$'
|
||||
|
||||
### app_list()
|
||||
list:
|
||||
|
@ -290,7 +301,8 @@ app:
|
|||
-u:
|
||||
full: --user
|
||||
help: Allowed app map for a user
|
||||
pattern: '^[a-z0-9_]+$'
|
||||
extra:
|
||||
pattern: '^[a-z0-9_]+$'
|
||||
|
||||
|
||||
### app_install() TODO: Write help
|
||||
|
@ -375,7 +387,8 @@ app:
|
|||
arguments:
|
||||
port:
|
||||
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()
|
||||
checkurl:
|
||||
|
@ -644,8 +657,9 @@ service:
|
|||
-n:
|
||||
full: --number
|
||||
help: Number of lines to display
|
||||
pattern: '^[0-9]+$'
|
||||
default: "50"
|
||||
extra:
|
||||
pattern: '^[0-9]+$'
|
||||
|
||||
|
||||
#############################
|
||||
|
@ -676,7 +690,8 @@ firewall:
|
|||
arguments:
|
||||
port:
|
||||
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:
|
||||
help: Protocol associated with port
|
||||
choices:
|
||||
|
@ -810,12 +825,12 @@ tools:
|
|||
arguments:
|
||||
-o:
|
||||
full: --old-password
|
||||
ask: "Current admin password"
|
||||
password: yes
|
||||
extra:
|
||||
password: "Current admin password"
|
||||
-n:
|
||||
full: --new-password
|
||||
ask: "New admin password"
|
||||
password: yes
|
||||
extra:
|
||||
password: "New admin password"
|
||||
|
||||
### tools_maindomain()
|
||||
maindomain:
|
||||
|
@ -824,11 +839,13 @@ tools:
|
|||
arguments:
|
||||
-o:
|
||||
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:
|
||||
full: --new-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])*)$'
|
||||
extra:
|
||||
ask: "New main domain"
|
||||
pattern: '^([a-zA-Z0-9]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)(\.[a-zA-Z0-9]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)*(\.[a-zA-Z]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)$'
|
||||
|
||||
### tools_postinstall()
|
||||
postinstall:
|
||||
|
@ -838,13 +855,14 @@ tools:
|
|||
-d:
|
||||
full: --domain
|
||||
help: YunoHost 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])*)$'
|
||||
extra:
|
||||
ask: "Main domain"
|
||||
pattern: '^([a-zA-Z0-9]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)(\.[a-zA-Z0-9]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)*(\.[a-zA-Z]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)$'
|
||||
-p:
|
||||
full: --password
|
||||
help: YunoHost admin password
|
||||
ask: "New admin password"
|
||||
password: yes
|
||||
extra:
|
||||
password: "New admin password"
|
||||
--dyndns:
|
||||
help: Subscribe domain to a DynDNS service
|
||||
action: store_true
|
||||
|
|
|
@ -46,8 +46,9 @@ def api(port, routes={}, use_cache=True):
|
|||
from bottle import run
|
||||
from core.actionsmap import ActionsMap
|
||||
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)
|
||||
|
||||
run(moulinette.app, port=port)
|
||||
|
@ -67,10 +68,11 @@ def cli(args, use_cache=True):
|
|||
"""
|
||||
import os
|
||||
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'
|
||||
|
||||
# TODO: Move the lock checking into the ActionsMap class
|
||||
# Check the lock
|
||||
if os.path.isfile(lock_file):
|
||||
raise YunoHostError(1, _("The moulinette is already running"))
|
||||
|
@ -80,7 +82,7 @@ def cli(args, use_cache=True):
|
|||
os.system('chmod 400 '+ lock_file)
|
||||
|
||||
try:
|
||||
amap = ActionsMap(ActionsMap.IFACE_CLI, use_cache=use_cache)
|
||||
amap = ActionsMap(Interface.cli, use_cache=use_cache)
|
||||
pretty_print_dict(amap.process(args))
|
||||
except KeyboardInterrupt, EOFError:
|
||||
raise YunoHostError(125, _("Interrupted"))
|
||||
|
|
|
@ -1,16 +1,21 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import argparse
|
||||
import getpass
|
||||
import marshal
|
||||
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 helpers import YunoHostError, colorize
|
||||
|
||||
from extraparameters import extraparameters_list
|
||||
from helpers import Interface, YunoHostError
|
||||
|
||||
## Additional parsers
|
||||
|
||||
class _HTTPArgumentParser(object):
|
||||
|
||||
|
@ -137,80 +142,75 @@ class HTTPParser(object):
|
|||
|
||||
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']
|
||||
API_PARAMETERS = ['pattern']
|
||||
AVAILABLE_PARAMETERS = CLI_PARAMETERS
|
||||
# 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 __init__(self, **kwargs):
|
||||
self._params = {}
|
||||
def validate(self, arg_name, parameters):
|
||||
"""
|
||||
Validate values of extra parameters for an argument
|
||||
|
||||
for k, v in kwargs.items():
|
||||
if k in self.AVAILABLE_PARAMETERS:
|
||||
self._params[k] = v
|
||||
Keyword arguments:
|
||||
- arg_name -- The argument name
|
||||
- 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():
|
||||
func = getattr(self, 'process_' + p)
|
||||
# Validate parameter value
|
||||
parameters[p] = self.extra[p].validate(v, arg_name)
|
||||
|
||||
if isinstance(ret, list):
|
||||
for p_v in p_value:
|
||||
r = func(v, p_name, p_v)
|
||||
if r is not None:
|
||||
ret.append(r)
|
||||
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:
|
||||
r = func(v, p_name, p_value)
|
||||
if r is not None:
|
||||
ret = r
|
||||
arg_value = parser(parameters[p], arg_name, arg_value)
|
||||
|
||||
return ret
|
||||
return arg_value
|
||||
|
||||
|
||||
## Parameters validating's method
|
||||
# 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
|
||||
|
||||
## Main class
|
||||
|
||||
class ActionsMap(object):
|
||||
"""
|
||||
|
@ -231,30 +231,37 @@ class ActionsMap(object):
|
|||
instead of using the cached one.
|
||||
|
||||
"""
|
||||
IFACE_CLI = 'cli'
|
||||
IFACE_API = 'api'
|
||||
|
||||
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))
|
||||
self.interface = interface
|
||||
self.use_cache = use_cache
|
||||
|
||||
logging.debug("initializing ActionsMap for the '%s' interface" % interface)
|
||||
|
||||
# Iterate over actions map namespaces
|
||||
actionsmap = {}
|
||||
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:
|
||||
actionsmap[n] = pickle.load(f)
|
||||
actionsmaps[n] = pickle.load(f)
|
||||
else:
|
||||
actionsmap = self.generate_cache()
|
||||
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:
|
||||
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):
|
||||
"""
|
||||
|
@ -268,9 +275,9 @@ class ActionsMap(object):
|
|||
arguments = None
|
||||
|
||||
# Parse arguments
|
||||
if self.interface ==self.IFACE_CLI:
|
||||
if self.interface == Interface.cli:
|
||||
arguments = self.parser.parse_args(args)
|
||||
elif self.interface ==self.IFACE_API:
|
||||
elif self.interface == Interface.api:
|
||||
if route is None:
|
||||
# TODO: Raise a proper exception
|
||||
raise Exception(_("Missing route argument"))
|
||||
|
@ -302,7 +309,10 @@ class ActionsMap(object):
|
|||
@staticmethod
|
||||
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 = []
|
||||
|
@ -313,65 +323,77 @@ class ActionsMap(object):
|
|||
return namespaces
|
||||
|
||||
@classmethod
|
||||
def generate_cache(cls):
|
||||
def generate_cache(klass):
|
||||
"""
|
||||
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):
|
||||
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)
|
||||
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)
|
||||
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
|
||||
|
||||
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:
|
||||
- parser -- Parser object of the argument
|
||||
- parser -- Parser object for the arguments
|
||||
- arg_name -- Argument name
|
||||
- arg_params -- Argument parameters
|
||||
|
||||
Returns:
|
||||
The parser object
|
||||
|
||||
"""
|
||||
params = {}
|
||||
keys = []
|
||||
|
||||
# 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
|
||||
if 'extra' in arg_params:
|
||||
# Retrieve current extra parameters dict
|
||||
extra = parser.get_default('_extra')
|
||||
if not extra or not isinstance(extra, dict):
|
||||
extra = {}
|
||||
|
||||
# Add completed extra parameters to the parser
|
||||
extra[arg_name] = _ExtraParameters(**params)
|
||||
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
|
||||
|
@ -379,41 +401,41 @@ class ActionsMap(object):
|
|||
del args['_extra']
|
||||
|
||||
# Validate extra parameters for each arguments
|
||||
for n, e in extra.items():
|
||||
args[n] = e.validate(n, args[n])
|
||||
for an, parameters in extra.items():
|
||||
args[an] = self.extraparser.parse(an, args[an], parameters)
|
||||
|
||||
return args
|
||||
|
||||
def _construct_parser(self, actionsmap):
|
||||
def _construct_parser(self, actionsmaps):
|
||||
"""
|
||||
Construct the parser with the actions map
|
||||
|
||||
Keyword arguments:
|
||||
- actionsmap -- Multi-level dictionnary of
|
||||
categories/actions/arguments list
|
||||
- actionsmaps -- A dict of multi-level dictionnary of
|
||||
categories/actions/arguments list for each namespaces
|
||||
|
||||
Returns:
|
||||
Interface relevant's parser object
|
||||
An interface relevant's parser object
|
||||
|
||||
"""
|
||||
top_parser = None
|
||||
iface = self.interface
|
||||
|
||||
# Create parser object
|
||||
if iface ==self.IFACE_CLI:
|
||||
# TODO: Add descritpion (from __description__)
|
||||
if iface == Interface.cli:
|
||||
# TODO: Add descritpion (from __description__?)
|
||||
top_parser = argparse.ArgumentParser()
|
||||
top_subparsers = top_parser.add_subparsers()
|
||||
elif iface ==self.IFACE_API:
|
||||
elif iface == Interface.api:
|
||||
top_parser = HTTPParser()
|
||||
|
||||
## Extract option strings from parameters
|
||||
## Format option strings from argument parameters
|
||||
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:
|
||||
return [arg_name, arg_params['full']]
|
||||
return [arg_name]
|
||||
elif iface ==self.IFACE_API:
|
||||
elif iface == Interface.api:
|
||||
if arg_name[0] != '-':
|
||||
return [arg_name]
|
||||
if 'full' in arg_params:
|
||||
|
@ -422,40 +444,31 @@ class ActionsMap(object):
|
|||
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
|
||||
def _clean_params(arg_params):
|
||||
keys = list(_ExtraParameters.AVAILABLE_PARAMETERS)
|
||||
keys.append('full')
|
||||
|
||||
for k in keys:
|
||||
for k in {'full', 'extra'}:
|
||||
if k in arg_params:
|
||||
del arg_params[k]
|
||||
return arg_params
|
||||
|
||||
# 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
|
||||
if iface ==self.IFACE_CLI:
|
||||
for an, ap in actionsmap[n]['general_arguments'].items():
|
||||
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[n]['general_arguments']
|
||||
del actionsmap['general_arguments']
|
||||
|
||||
# Parse categories
|
||||
for cn, cp in actionsmap[n].items():
|
||||
for cn, cp in actionsmap.items():
|
||||
if 'actions' not in cp:
|
||||
continue
|
||||
|
||||
# Add category subparsers for the cli only
|
||||
if iface ==self.IFACE_CLI:
|
||||
c_help = _key(cp, 'category_help')
|
||||
if iface == Interface.cli:
|
||||
c_help = cp.get('category_help')
|
||||
subparsers = top_subparsers.add_parser(cn, help=c_help).add_subparsers()
|
||||
|
||||
# Parse actions
|
||||
|
@ -463,10 +476,10 @@ class ActionsMap(object):
|
|||
parser = None
|
||||
|
||||
# Add parser for the current action
|
||||
if iface ==self.IFACE_CLI:
|
||||
a_help = _key(ap, 'action_help')
|
||||
if iface == Interface.cli:
|
||||
a_help = ap.get('action_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
|
||||
m = re.match('(GET|POST|PUT|DELETE) (/\S+)', ap['api'])
|
||||
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__:
|
||||
import traceback
|
||||
|
||||
|
||||
class Interface():
|
||||
"""
|
||||
Contain available interfaces to use with the moulinette.
|
||||
|
||||
"""
|
||||
api = 'api'
|
||||
cli = 'cli'
|
||||
|
||||
@classmethod
|
||||
def all(klass):
|
||||
"""Get a list of all interfaces"""
|
||||
ifaces = set(i for i in dir(klass) if not i.startswith('_'))
|
||||
return ifaces
|
||||
|
||||
win = []
|
||||
|
||||
def random_password(length=8):
|
||||
|
|
Loading…
Add table
Reference in a new issue