Standardize extra parameters and make them modular

This commit is contained in:
Jerome Lebleu 2014-02-24 18:28:11 +01:00
parent a2be4d6e12
commit 0bfc63e4b8
5 changed files with 374 additions and 168 deletions

View file

@ -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

View file

@ -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"))

View file

@ -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:

View 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}

View file

@ -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):