From 0bfc63e4b88330f79f846db81d914ed6c0593c14 Mon Sep 17 00:00:00 2001 From: Jerome Lebleu Date: Mon, 24 Feb 2014 18:28:11 +0100 Subject: [PATCH] Standardize extra parameters and make them modular --- etc/actionsmap/yunohost.yml | 78 ++++--- src/moulinette/__init__.py | 8 +- src/moulinette/core/actionsmap.py | 283 +++++++++++++------------ src/moulinette/core/extraparameters.py | 158 ++++++++++++++ src/moulinette/core/helpers.py | 15 ++ 5 files changed, 374 insertions(+), 168 deletions(-) create mode 100644 src/moulinette/core/extraparameters.py diff --git a/etc/actionsmap/yunohost.yml b/etc/actionsmap/yunohost.yml index cf2df613..fe5ab237 100644 --- a/etc/actionsmap/yunohost.yml +++ b/etc/actionsmap/yunohost.yml @@ -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 diff --git a/src/moulinette/__init__.py b/src/moulinette/__init__.py index 784ed407..f96eae48 100755 --- a/src/moulinette/__init__.py +++ b/src/moulinette/__init__.py @@ -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")) diff --git a/src/moulinette/core/actionsmap.py b/src/moulinette/core/actionsmap.py index 022dc06c..90ff9f43 100644 --- a/src/moulinette/core/actionsmap.py +++ b/src/moulinette/core/actionsmap.py @@ -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: diff --git a/src/moulinette/core/extraparameters.py b/src/moulinette/core/extraparameters.py new file mode 100644 index 00000000..3f4248c0 --- /dev/null +++ b/src/moulinette/core/extraparameters.py @@ -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} diff --git a/src/moulinette/core/helpers.py b/src/moulinette/core/helpers.py index cf4e383c..3d0517aa 100644 --- a/src/moulinette/core/helpers.py +++ b/src/moulinette/core/helpers.py @@ -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):