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

View file

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

View file

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

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