diff --git a/bin/yunohost b/bin/yunohost index fb03b34d..78a354bf 100755 --- a/bin/yunohost +++ b/bin/yunohost @@ -3,25 +3,21 @@ import sys import os.path -import gettext # Run from source -basedir = os.path.abspath(os.path.dirname(__file__) +'/../') -if os.path.isdir(basedir +'/src'): - sys.path.append(basedir +'/src') +basedir = os.path.abspath('%s/../' % os.path.dirname(__file__)) +if os.path.isdir('%s/src' % basedir): + sys.path.append('%s/src' % basedir) from moulinette import init, cli, MoulinetteError from moulinette.helpers import YunoHostError, colorize -gettext.install('yunohost') - ## Main action if __name__ == '__main__': - # Run from source (prefix and libdir set to None) - init('yunohost', prefix=None, libdir=None, - cachedir=os.path.join(basedir, 'cache')) + # Run from source + init(_from_source=True) # Additional arguments use_cache = True @@ -39,7 +35,7 @@ if __name__ == '__main__': raise YunoHostError(17, _("YunoHost is not correctly installed, please execute 'yunohost tools postinstall'")) # Execute the action - cli(args, use_cache) + cli(['yunohost'], args, use_cache) except MoulinetteError as e: print(e.colorize()) sys.exit(e.code) diff --git a/bin/yunohost-api b/bin/yunohost-api index 18d10d09..11e889c5 100755 --- a/bin/yunohost-api +++ b/bin/yunohost-api @@ -3,7 +3,6 @@ import sys import os.path -import gettext # Run from source basedir = os.path.abspath(os.path.dirname(__file__) +'/../') @@ -12,8 +11,6 @@ if os.path.isdir(basedir +'/src'): from moulinette import init, api -gettext.install('yunohost') - ## Callbacks for additional routes @@ -31,9 +28,8 @@ def is_installed(): ## Main action if __name__ == '__main__': - # Run from source (prefix and libdir set to None) - init('yunohost', prefix=None, libdir=None, - cachedir=os.path.join(basedir, 'cache')) + # Run from source + init(_from_source=True) # Additional arguments use_cache = True @@ -45,5 +41,5 @@ if __name__ == '__main__': # TODO: Add log argument # Rune the server - api(6787, {('GET', '/installed'): is_installed}, use_cache) + api(['yunohost'], 6787, {('GET', '/installed'): is_installed}, use_cache) sys.exit(0) diff --git a/src/moulinette/extra/__init__.py b/lib/yunohost/__init__.py old mode 100644 new mode 100755 similarity index 100% rename from src/moulinette/extra/__init__.py rename to lib/yunohost/__init__.py diff --git a/src/yunohost/app.py b/lib/yunohost/app.py similarity index 100% rename from src/yunohost/app.py rename to lib/yunohost/app.py diff --git a/src/yunohost/backup.py b/lib/yunohost/backup.py similarity index 100% rename from src/yunohost/backup.py rename to lib/yunohost/backup.py diff --git a/data/firewall.yml b/lib/yunohost/data/firewall.yml similarity index 100% rename from data/firewall.yml rename to lib/yunohost/data/firewall.yml diff --git a/data/ldap_scheme.yml b/lib/yunohost/data/ldap_scheme.yml similarity index 100% rename from data/ldap_scheme.yml rename to lib/yunohost/data/ldap_scheme.yml diff --git a/data/services.yml b/lib/yunohost/data/services.yml similarity index 100% rename from data/services.yml rename to lib/yunohost/data/services.yml diff --git a/src/yunohost/domain.py b/lib/yunohost/domain.py similarity index 100% rename from src/yunohost/domain.py rename to lib/yunohost/domain.py diff --git a/src/yunohost/dyndns.py b/lib/yunohost/dyndns.py similarity index 100% rename from src/yunohost/dyndns.py rename to lib/yunohost/dyndns.py diff --git a/src/yunohost/firewall.py b/lib/yunohost/firewall.py similarity index 100% rename from src/yunohost/firewall.py rename to lib/yunohost/firewall.py diff --git a/src/yunohost/hook.py b/lib/yunohost/hook.py similarity index 100% rename from src/yunohost/hook.py rename to lib/yunohost/hook.py diff --git a/src/yunohost/monitor.py b/lib/yunohost/monitor.py similarity index 100% rename from src/yunohost/monitor.py rename to lib/yunohost/monitor.py diff --git a/src/yunohost/service.py b/lib/yunohost/service.py similarity index 100% rename from src/yunohost/service.py rename to lib/yunohost/service.py diff --git a/src/yunohost/tools.py b/lib/yunohost/tools.py similarity index 100% rename from src/yunohost/tools.py rename to lib/yunohost/tools.py diff --git a/src/yunohost/user.py b/lib/yunohost/user.py similarity index 100% rename from src/yunohost/user.py rename to lib/yunohost/user.py diff --git a/src/moulinette/__init__.py b/src/moulinette/__init__.py index aa8c6753..0bc0ff72 100755 --- a/src/moulinette/__init__.py +++ b/src/moulinette/__init__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- __title__ = 'moulinette' -__version__ = '695' +__version__ = '0.1' __author__ = ['Kload', 'jlebleu', 'titoko', @@ -31,12 +31,10 @@ __all__ = [ from .core import MoulinetteError -curr_namespace = None - ## Package functions -def init(namespace=None, **kwargs): +def init(**kwargs): """Package initialization Initialize directories and global variables. It must be called @@ -44,30 +42,33 @@ def init(namespace=None, **kwargs): functions. Keyword arguments: - - namespace -- The namespace to initialize and use - - **kwargs -- See helpers.Package + - **kwargs -- See core.Package At the end, the global variable 'pkg' will contain a Package - instance. See helpers.Package for available methods and variables. + instance. See core.Package for available methods and variables. """ + import sys import __builtin__ - from .core import Package - - global curr_namespace - curr_namespace = namespace - + from .core import Package, install_i18n __builtin__.__dict__['pkg'] = Package(**kwargs) + # Initialize internationalization + install_i18n() + + # Add library directory to python path + sys.path.append(pkg.libdir) + ## Easy access to interfaces -def api(port, routes={}, use_cache=True): +def api(namespaces, port, routes={}, use_cache=True): """Web server (API) interface Run a HTTP server with the moulinette for an API usage. Keyword arguments: + - namespaces -- The list of namespaces to use - port -- Port to run on - routes -- A dict of additional routes to add in the form of {(method, uri): callback} @@ -79,18 +80,19 @@ def api(port, routes={}, use_cache=True): from .actionsmap import ActionsMap from .interface.api import MoulinetteAPI - amap = ActionsMap('api', use_cache=use_cache) + amap = ActionsMap('api', namespaces, use_cache) moulinette = MoulinetteAPI(amap, routes) run(moulinette.app, port=port) -def cli(args, use_cache=True): +def cli(namespaces, args, use_cache=True): """Command line interface Execute an action with the moulinette from the CLI and print its result in a readable format. Keyword arguments: + - namespaces -- The list of namespaces to use - args -- A list of argument strings - use_cache -- False if it should parse the actions map file instead of using the cached one @@ -98,7 +100,7 @@ def cli(args, use_cache=True): """ import os from .actionsmap import ActionsMap - from .helpers import YunoHostError, pretty_print_dict + from .helpers import pretty_print_dict lock_file = '/var/run/moulinette.lock' @@ -112,7 +114,7 @@ def cli(args, use_cache=True): os.system('chmod 400 '+ lock_file) try: - amap = ActionsMap('cli', use_cache=use_cache) + amap = ActionsMap('cli', namespaces, use_cache) pretty_print_dict(amap.process(args)) except KeyboardInterrupt, EOFError: raise MoulinetteError(125, _("Interrupted")) diff --git a/src/moulinette/actionsmap.py b/src/moulinette/actionsmap.py index f2eb5464..db07318b 100644 --- a/src/moulinette/actionsmap.py +++ b/src/moulinette/actionsmap.py @@ -1,21 +1,478 @@ # -*- coding: utf-8 -*- -import pickle +import argparse import yaml import re import os +import cPickle as pickle from collections import OrderedDict import logging -from . import __version__, curr_namespace, MoulinetteError -from .extra.parameters import extraparameters_list +from . import __version__ +from .core import MoulinetteError -## Extra parameters Parser +## Interfaces' Actions map Parser -------------------------------------- + +class _AMapParser(object): + """Actions map's base Parser + + Each interfaces must implement a parser class derived from this + class. It is used to parse the main parts of the actions map (i.e. + general arguments, categories and actions). -class ExtraParser(object): """ - Global parser for the extra parameters. + + ## Optional variables + # Each parser classes can overwrite these variables. + + """Either it will parse general arguments, or not""" + parse_general_arguments = True + + + ## Virtual methods + # Each parser classes can implement these methods. + + @staticmethod + def format_arg_name(name, full): + """Format argument name + + Format agument name depending on its 'full' parameters and return + a list to use it as option string for the argument parser. + + Keyword arguments: + - name -- The argument name + - full -- The argument's 'full' parameter + + Returns: + A list of option strings + + """ + raise NotImplementedError("derived class '%s' must override this method" % \ + self.__class__.__name__) + + def add_general_parser(self, **kwargs): + """Add a parser for general arguments + + Create and return an argument parser for general arguments. + + Returns: + An ArgumentParser based object + + """ + if not self.parse_general_arguments: + msg = "doesn't parse general arguments" + else: + msg = "must override this method" + raise NotImplementedError("derived class '%s' %s" % \ + (self.__class__.__name__, msg)) + + def add_category_parser(self, name, **kwargs): + """Add a parser for a category + + Create a new category and return a parser for it. + + Keyword arguments: + - name -- The category name + + Returns: + A BaseParser based object + + """ + raise NotImplementedError("derived class '%s' must override this method" % \ + self.__class__.__name__) + + def add_action_parser(self, name, **kwargs): + """Add a parser for an action + + Create a new action and return an argument parser for it. + + Keyword arguments: + - name -- The action name + + Returns: + An ArgumentParser based object + + """ + raise NotImplementedError("derived class '%s' must override this method" % \ + self.__class__.__name__) + + def parse_args(self, args, **kwargs): + """Parse arguments + + Convert argument variables to objects and assign them as + attributes of the namespace. + + Keyword arguments: + - args -- Arguments string or dict (TODO) + + Returns: + The populated namespace + + """ + raise NotImplementedError("derived class '%s' must override this method" % \ + self.__class__.__name__) + +# CLI Actions map Parser + +class CLIAMapParser(_AMapParser): + """Actions map's CLI Parser + + """ + def __init__(self, parser=None): + self._parser = parser or argparse.ArgumentParser() + self._subparsers = self._parser.add_subparsers() + + @staticmethod + def format_arg_name(name, full): + if name[0] == '-' and full: + return [name, full] + return [name] + + def add_general_parser(self, **kwargs): + return self._parser + + def add_category_parser(self, name, category_help=None, **kwargs): + """Add a parser for a category + + Keyword arguments: + - category_help -- A brief description for the category + + Returns: + A new CLIParser object for the category + + """ + parser = self._subparsers.add_parser(name, help=category_help) + return self.__class__(parser) + + def add_action_parser(self, name, action_help, **kwargs): + """Add a parser for an action + + Keyword arguments: + - action_help -- A brief description for the action + + Returns: + A new argparse.ArgumentParser object for the action + + """ + return self._subparsers.add_parser(name, help=action_help) + + def parse_args(self, args, **kwargs): + return self._parser.parse_args(args) + +# API Actions map Parser + +class _HTTPArgumentParser(object): + """Argument parser for HTTP requests + + Object for parsing HTTP requests into Python objects. It is based + on argparse.ArgumentParser class and implements some of its methods. + + """ + def __init__(self): + # Initialize the ArgumentParser object + self._parser = argparse.ArgumentParser(usage='', + prefix_chars='@', + add_help=False) + self._parser.error = self._error + + self._positional = [] # list(arg_name) + self._optional = {} # dict({arg_name: option_strings}) + + def set_defaults(self, **kwargs): + return self._parser.set_defaults(**kwargs) + + def get_default(self, dest): + return self._parser.get_default(dest) + + def add_argument(self, *args, **kwargs): + action = self._parser.add_argument(*args, **kwargs) + + # Append newly created action + if len(action.option_strings) == 0: + self._positional.append(action.dest) + else: + self._optional[action.dest] = action.option_strings + + return action + + def parse_args(self, args): + arg_strings = [] + + ## Append an argument to the current one + def append(arg_strings, value, option_string=None): + # TODO: Process list arguments + if isinstance(value, bool): + # Append the option string only + if option_string is not None: + arg_strings.append(option_string) + elif isinstance(value, str): + if option_string is not None: + arg_strings.append(option_string) + arg_strings.append(value) + else: + arg_strings.append(value) + + return arg_strings + + # Iterate over positional arguments + for dest in self._positional: + if dest in args: + arg_strings = append(arg_strings, args[dest]) + + # Iterate over optional arguments + for dest, opt in self._optional.items(): + if dest in args: + arg_strings = append(arg_strings, args[dest], opt[0]) + return self._parser.parse_args(arg_strings) + + def _error(self, message): + # TODO: Raise a proper exception + raise MoulinetteError(1, message) + +class APIAMapParser(_AMapParser): + """Actions map's API Parser + + """ + parse_general_arguments = False + + def __init__(self): + self._parsers = {} # dict({(method, path): _HTTPArgumentParser}) + + @property + def routes(self): + """Get current routes""" + return self._parsers.keys() + + + ## Implement virtual methods + + @staticmethod + def format_arg_name(name, full): + if name[0] != '-': + return [name] + if full: + return [full.replace('--', '@', 1)] + if name.startswith('--'): + return [name.replace('--', '@', 1)] + return [name.replace('-', '@', 1)] + + def add_category_parser(self, name, **kwargs): + return self + + def add_action_parser(self, name, api=None, **kwargs): + """Add a parser for an action + + Keyword arguments: + - api -- The action route (e.g. 'GET /' ) + + Returns: + A new _HTTPArgumentParser object for the route + + """ + if not api: + return None + + # Validate action route + m = re.match('(GET|POST|PUT|DELETE) (/\S+)', api) + if not m: + return None + + # Check if a parser already exists for the route + key = (m.group(1), m.group(2)) + if key in self.routes: + raise ValueError("A parser for '%s' already exists" % key) + + # Create and append parser + parser = _HTTPArgumentParser() + self._parsers[key] = parser + + # Return the created parser + return parser + + def parse_args(self, args, route, **kwargs): + """Parse arguments + + Keyword arguments: + - route -- The action route (e.g. 'GET /' ) + + """ + # Retrieve the parser for the route + if route not in self.routes: + raise MoulinetteError(22, "No parser for '%s %s' found" % key) + + return self._parsers[route].parse_args(args) + +""" +The dict of interfaces names and their associated parser class. + +""" +actionsmap_parsers = { + 'api': APIAMapParser, + 'cli': CLIAMapParser +} + + +## Extra parameters ---------------------------------------------------- + +# Extra parameters definition + +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 + +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 = { '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 MoulinetteError(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 MoulinetteError(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} + +# Extra parameters argument Parser + +class ExtraArgumentParser(object): + """ + Argument validator and parser for the extra parameters. + + Keyword arguments: + - iface -- The running interface """ def __init__(self, iface): @@ -78,69 +535,64 @@ class ExtraParser(object): return arg_value -## Main class +## Main class ---------------------------------------------------------- class ActionsMap(object): - """ - Validate and process action defined into the actions map. + """Validate and process actions defined into an actions map - The actions map defines features and their usage of the main + The actions map defines the features and their usage of the main application. It is composed by categories which contain one or more action(s). Moreover, the action can have specific argument(s). - Keyword arguments: + This class allows to manipulate one or several actions maps + associated to a namespace. If no namespace is given, it will load + all available namespaces. - - interface -- Interface type that requires the actions map. - Possible value is one of: + Keyword arguments: + - interface -- The type of interface which needs the actions map. + Possible values are: - 'cli' for the command line interface - 'api' for an API usage (HTTP requests) - + - namespaces -- The list of namespaces to use - use_cache -- False if it should parse the actions map file instead of using the cached one. """ - def __init__(self, interface, use_cache=True): + def __init__(self, interface, namespaces=[], use_cache=True): self.use_cache = use_cache try: # Retrieve the interface parser - mod = __import__('interface.%s' % interface, - globals=globals(), level=1, - fromlist=['actionsmap_parser']) - parser = getattr(mod, 'actionsmap_parser') - except (AttributeError, ImportError): + self._parser_class = actionsmap_parsers[interface] + except KeyError: raise MoulinetteError(22, _("Invalid interface '%s'" % interface)) - else: - self._parser_class = parser logging.debug("initializing ActionsMap for the '%s' interface" % interface) + if len(namespaces) == 0: + namespaces = self.get_namespaces() actionsmaps = {} - namespaces = self.get_actionsmap_namespaces() - if curr_namespace and curr_namespace in namespaces: - namespaces = [curr_namespace] # Iterate over actions map namespaces for n in namespaces: logging.debug("loading '%s' actions map namespace" % n) if use_cache: - # Attempt to load cache if it exists - cache_file = '%s/%s.pkl' % (pkg.cachedir('actionsmap'), n) - if os.path.isfile(cache_file): - with open(cache_file, 'r') as f: + try: + # Attempt to load cache + with open('%s/actionsmap/%s.pkl' % (pkg.cachedir, n)) as f: actionsmaps[n] = pickle.load(f) - else: + # TODO: Switch to python3 and catch proper exception + except IOError: self.use_cache = False actionsmaps = self.generate_cache(namespaces) break else: - am_file = '%s/%s.yml' % (pkg.datadir('actionsmap'), n) - with open(am_file, 'r') as f: + with open('%s/actionsmap/%s.yml' % (pkg.datadir, n)) as f: actionsmaps[n] = yaml.load(f) # Generate parsers - self.extraparser = ExtraParser(interface) + self.extraparser = ExtraArgumentParser(interface) self.parser = self._construct_parser(actionsmaps) def process(self, args, **kwargs): @@ -152,6 +604,8 @@ class ActionsMap(object): - **kwargs -- Additional interface arguments """ + # Check moulinette status + # Parse arguments arguments = vars(self.parser.parse_args(args, **kwargs)) arguments = self._parse_extra_parameters(arguments) @@ -170,11 +624,12 @@ class ActionsMap(object): else: # Process the action return func(**arguments) + return {} @staticmethod - def get_actionsmap_namespaces(): + def get_namespaces(): """ - Retrieve actions map namespaces from a given path + Retrieve available actions map namespaces Returns: A list of available namespaces @@ -182,7 +637,7 @@ class ActionsMap(object): """ namespaces = [] - for f in os.listdir(pkg.datadir('actionsmap')): + for f in os.listdir('%s/actionsmap' % pkg.datadir): if f.endswith('.yml'): namespaces.append(f[:-4]) return namespaces @@ -201,20 +656,19 @@ class ActionsMap(object): """ actionsmaps = {} if not namespaces: - namespaces = klass.get_actionsmap_namespaces() + namespaces = klass.get_namespaces() # Iterate over actions map namespaces for n in namespaces: logging.debug("generating cache for '%s' actions map namespace" % n) # Read actions map from yaml file - am_file = pkg.datafile('actionsmap/%s.yml' % n) + am_file = '%s/actionsmap/%s.yml' % (pkg.datadir, n) with open(am_file, 'r') as f: actionsmaps[n] = yaml.load(f) # Cache actions map into pickle file - cache_file = pkg.cachefile('actionsmap/%s.pkl' % n, make_dir=True) - with open(cache_file, 'w') as f: + with pkg.open_cache('%s.pkl' % n, subdir='actionsmap') as f: pickle.dump(actionsmaps[n], f) return actionsmaps @@ -291,41 +745,41 @@ class ActionsMap(object): for n, actionsmap in actionsmaps.items(): if 'general_arguments' in actionsmap: # Parse general arguments - if top_parser.parse_general: + if top_parser.parse_general_arguments: parser = top_parser.add_general_parser() for an, ap in actionsmap['general_arguments'].items(): - if 'version' in ap: - ap['version'] = ap['version'].replace('%version%', - __version__) + # Replace version number + version = ap.get('version', None) + if version: + ap['version'] = version.replace('%version%', + __version__) argname = top_parser.format_arg_name(an, ap.pop('full', None)) parser.add_argument(*argname, **ap) del actionsmap['general_arguments'] # Parse categories for cn, cp in actionsmap.items(): - if 'actions' not in cp: + try: + actions = cp.pop('actions') + except KeyError: continue - actions = cp.pop('actions') # Add category parser - if top_parser.parse_category: - cat_parser = top_parser.add_category_parser(cn, **cp) - else: - cat_parser = top_parser + cat_parser = top_parser.add_category_parser(cn, **cp) # Parse actions - if not top_parser.parse_action: - continue for an, ap in actions.items(): arguments = ap.pop('arguments', {}) # Add action parser parser = cat_parser.add_action_parser(an, **ap) - if not parser: - continue - # Store action information - parser.set_defaults(_info=(n, cn, an)) + try: + # Store action information + parser.set_defaults(_info=(n, cn, an)) + except AttributeError: + # No parser for the action + break # Add action arguments for argn, argp in arguments.items(): diff --git a/src/moulinette/core.py b/src/moulinette/core.py index 62fe34fe..5dbbe557 100644 --- a/src/moulinette/core.py +++ b/src/moulinette/core.py @@ -5,115 +5,126 @@ import sys import gettext from .helpers import colorize +# Package manipulation ------------------------------------------------- + +def install_i18n(namespace=None): + """Install internationalization + + Install translation based on the package's default gettext domain or + on 'namespace' if provided. + + Keyword arguments: + - namespace -- The namespace to initialize i18n for + + """ + if namespace: + try: + t = gettext.translation(namespace, pkg.localedir) + except IOError: + # TODO: Log error + return + else: + t.install() + else: + gettext.install('moulinette', pkg.localedir) + class Package(object): - """Package representation and easy access + """Package representation and easy access methods Initialize directories and variables for the package and give them easy access. Keyword arguments: - - prefix -- The installation prefix - - libdir -- The library directory; usually, this would be - prefix + '/lib' (or '/lib64') when installed - - cachedir -- The cache directory; usually, this would be - '/var/cache' when installed - - destdir -- The destination prefix only if it's an installation - - 'prefix' and 'libdir' arguments should be empty in order to run - package from source. + - _from_source -- Either the package is running from source or + not (only for debugging) """ - def __init__(self, prefix, libdir, cachedir, destdir=None): - if not prefix and not libdir: - # Running from source directory + def __init__(self, _from_source=False): + if _from_source: + import sys basedir = os.path.abspath(os.path.dirname(sys.argv[0]) +'/../') - self._datadir = os.path.join(basedir, 'data') - self._libdir = os.path.join(basedir, 'src') - self._cachedir = cachedir + + # Set local directories + self._datadir = '%s/data' % basedir + self._libdir = '%s/lib' % basedir + self._localedir = '%s/po' % basedir + self._cachedir = '%s/cache' % basedir else: - self._datadir = os.path.join(prefix, 'share/moulinette') - self._libdir = os.path.join(libdir, 'moulinette') - self._cachedir = os.path.join(cachedir, 'moulinette') + import package - # Append library path to python's path - sys.path.append(self._libdir) - self._destdir = destdir or None + # Set system directories + self._datadir = package.datadir + self._libdir = package.libdir + self._localedir = package.localedir + self._cachedir = package.cachedir + + def __setattr__(self, name, value): + if name[0] == '_' and self.__dict__.has_key(name): + # Deny reassignation of package directories + raise TypeError("cannot reassign constant '%s'") + self.__dict__[name] = value - ## Easy access to directories and files + ## Easy access to package directories - def datadir(self, subdir=None, **kwargs): - """Return the path to a data directory""" - return self.get_dir(self._datadir, subdir, **kwargs) + @property + def datadir(self): + """Return the data directory of the package""" + return self._datadir - def datafile(self, filename, **kwargs): - """Return the path to a data file""" - return self.get_file(self._datadir, filename, **kwargs) + @property + def libdir(self): + """Return the lib directory of the package""" + return self._libdir - def libdir(self, subdir=None, **kwargs): - """Return the path to a lib directory""" - return self.get_dir(self._libdir, subdir, **kwargs) + @property + def localedir(self): + """Return the locale directory of the package""" + return self._localedir - def libfile(self, filename, **kwargs): - """Return the path to a lib file""" - return self.get_file(self._libdir, filename, **kwargs) - - def cachedir(self, subdir=None, **kwargs): - """Return the path to a cache directory""" - return self.get_dir(self._cachedir, subdir, **kwargs) - - def cachefile(self, filename, **kwargs): - """Return the path to a cache file""" - return self.get_file(self._cachedir, filename, **kwargs) + @property + def cachedir(self): + """Return the cache directory of the package""" + return self._cachedir - ## Standard methods + ## Additional methods - def get_dir(self, basedir, subdir=None, make_dir=False): - """Get a directory path + def get_cachedir(self, subdir='', make_dir=True): + """Get the path to a cache directory - Return a path composed by a base directory and an optional - subdirectory. The path will be created if needed. + Return the path to the cache directory from an optional + subdirectory and create it if needed. Keyword arguments: - - basedir -- The base directory - - subdir -- An optional subdirectory - - make_dir -- True if it should create needed directory + - subdir -- A cache subdirectory + - make_dir -- False to not make directory if it not exists """ - # Retrieve path - path = basedir - if self._destdir: - path = os.path.join(self._destdir, path) - if subdir: - path = os.path.join(path, subdir) + path = os.path.join(self.cachedir, subdir) - # Create directory if make_dir and not os.path.isdir(path): os.makedirs(path) return path - def get_file(self, basedir, filename, **kwargs): - """Get a file path + def open_cache(self, filename, subdir='', mode='w'): + """Open a cache file and return a stream - Return the path of the filename in the specified directory. This - directory will be created if needed. + Attempt to open in 'mode' the cache file 'filename' from the + default cache directory and in the subdirectory 'subdir' if + given. Directories are created if needed and a stream is + returned if the file can be written. Keyword arguments: - - basedir -- The base directory of the file - - filename -- The filename or a path relative to basedir - - **kwargs -- Additional arguments for Package.get_dir + - filename -- The cache filename + - subdir -- A subdirectory which contains the file + - mode -- The mode in which the file is opened """ - # Check for a directory in filename - subdir = os.path.dirname(filename) or None - if subdir: - filename = os.path.basename(filename) + return open('%s/%s' % (self.get_cachedir(subdir), filename), mode) - # Get directory path - dirpath = self.get_dir(basedir, subdir, **kwargs) - return os.path.join(dirpath, filename) +# Moulinette core classes ---------------------------------------------- class MoulinetteError(Exception): """Moulinette base exception diff --git a/src/moulinette/extra/parameters.py b/src/moulinette/extra/parameters.py deleted file mode 100644 index f1bf0ba9..00000000 --- a/src/moulinette/extra/parameters.py +++ /dev/null @@ -1,159 +0,0 @@ -# -*- coding: utf-8 -*- - -import getpass -import re -import logging - -from .. import MoulinetteError -from ..helpers import colorize - -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 = { '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 MoulinetteError(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 MoulinetteError(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/helpers.py b/src/moulinette/helpers.py index 69efaf1f..d632d03d 100644 --- a/src/moulinette/helpers.py +++ b/src/moulinette/helpers.py @@ -1,25 +1,15 @@ # -*- coding: utf-8 -*- import os -import sys -try: - import ldap -except ImportError: - sys.stderr.write('Error: Yunohost CLI Require LDAP lib\n') - sys.stderr.write('apt-get install python-ldap\n') - sys.exit(1) +import ldap import ldap.modlist as modlist -import yaml import json import re import getpass import random import string -import argparse import gettext import getpass -if not __debug__: - import traceback win = [] diff --git a/src/moulinette/interface/__init__.py b/src/moulinette/interface/__init__.py old mode 100755 new mode 100644 index 3a99535f..e69de29b --- a/src/moulinette/interface/__init__.py +++ b/src/moulinette/interface/__init__.py @@ -1,114 +0,0 @@ -# -*- coding: utf-8 -*- - -class BaseParser(object): - """Actions map's base Parser - - Each interfaces must implement a parser class derived from this - class. It is used to parse the main parts of the actions map (i.e. - general arguments, categories and actions). - - """ - - ## Optional variables - # Each parser classes can overwrite these variables. - - """Either it will parse general arguments, or not""" - parse_general = True - - """Either it will parse categories, or not""" - parse_category = True - - """Either it will parse actions, or not""" - parse_action = True - - - ## Virtual methods - # Each parser classes can implement these methods. - - @staticmethod - def format_arg_name(name, full): - """Format argument name - - Format agument name depending on its 'full' parameters and return - a list to use it as option string for the argument parser. - - Keyword arguments: - - name -- The argument name - - full -- The argument's 'full' parameter - - Returns: - A list of option strings - - """ - raise NotImplementedError("derived class '%s' must override this method" % \ - self.__class__.__name__) - - def add_general_parser(self, **kwargs): - """Add a parser for general arguments - - Create and return an argument parser for general arguments. - - Returns: - An ArgumentParser based object - - """ - if not self.parse_general: - msg = "doesn't parse general arguments" - else: - msg = "must override this method" - raise NotImplementedError("derived class '%s' %s" % \ - (self.__class__.__name__, msg)) - - def add_category_parser(self, name, **kwargs): - """Add a parser for a category - - Create a new category and return a parser for it. - - Keyword arguments: - - name -- The category name - - Returns: - A BaseParser based object - - """ - if not self.parse_categories: - msg = "doesn't parse categories" - else: - msg = "must override this method" - raise NotImplementedError("derived class '%s' %s" % \ - (self.__class__.__name__, msg)) - - def add_action_parser(self, name, **kwargs): - """Add a parser for an action - - Create a new action and return an argument parser for it. - - Keyword arguments: - - name -- The action name - - Returns: - An ArgumentParser based object - - """ - if not self.parse_general: - msg = "doesn't parse actions" - else: - msg = "must override this method" - raise NotImplementedError("derived class '%s' %s" % \ - (self.__class__.__name__, msg)) - - def parse_args(self, args, **kwargs): - """Parse arguments - - Convert argument variables to objects and assign them as - attributes of the namespace. - - Keyword arguments: - - args -- Arguments string or dict (TODO) - - Returns: - The populated namespace - - """ - raise NotImplementedError("derived class '%s' must override this method" % \ - self.__class__.__name__) diff --git a/src/moulinette/interface/api.py b/src/moulinette/interface/api.py index 6c959c9f..60e72228 100644 --- a/src/moulinette/interface/api.py +++ b/src/moulinette/interface/api.py @@ -1,160 +1,12 @@ # -*- coding: utf-8 -*- -import re -import argparse -import os.path from bottle import Bottle, request, response, HTTPResponse from beaker.middleware import SessionMiddleware -from . import BaseParser -from .. import MoulinetteError +from ..core import MoulinetteError from ..helpers import YunoHostError, YunoHostLDAP -## API arguments Parser - -class _HTTPArgumentParser(object): - """Argument parser for HTTP requests - - Object for parsing HTTP requests into Python objects. It is based - on argparse.ArgumentParser class and implements some of its methods. - - """ - def __init__(self): - # Initialize the ArgumentParser object - self._parser = argparse.ArgumentParser(usage='', - prefix_chars='@', - add_help=False) - self._parser.error = self._error - - self._positional = [] # list(arg_name) - self._optional = {} # dict({arg_name: option_strings}) - - def set_defaults(self, **kwargs): - return self._parser.set_defaults(**kwargs) - - def get_default(self, dest): - return self._parser.get_default(dest) - - def add_argument(self, *args, **kwargs): - action = self._parser.add_argument(*args, **kwargs) - - # Append newly created action - if len(action.option_strings) == 0: - self._positional.append(action.dest) - else: - self._optional[action.dest] = action.option_strings - - return action - - def parse_args(self, args): - arg_strings = [] - - ## Append an argument to the current one - def append(arg_strings, value, option_string=None): - # TODO: Process list arguments - if isinstance(value, bool): - # Append the option string only - if option_string is not None: - arg_strings.append(option_string) - elif isinstance(value, str): - if option_string is not None: - arg_strings.append(option_string) - arg_strings.append(value) - else: - arg_strings.append(value) - - return arg_strings - - # Iterate over positional arguments - for dest in self._positional: - if dest in args: - arg_strings = append(arg_strings, args[dest]) - - # Iterate over optional arguments - for dest, opt in self._optional.items(): - if dest in args: - arg_strings = append(arg_strings, args[dest], opt[0]) - return self._parser.parse_args(arg_strings) - - def _error(self, message): - # TODO: Raise a proper exception - raise MoulinetteError(1, message) - -class APIParser(BaseParser): - """Actions map's API Parser - - """ - parse_category = False - parse_general = False - - def __init__(self): - self._parsers = {} # dict({(method, path): _HTTPArgumentParser}) - - @property - def routes(self): - """Get current routes""" - return self._parsers.keys() - - - ## Implement virtual methods - - @staticmethod - def format_arg_name(name, full): - if name[0] != '-': - return [name] - if full: - return [full.replace('--', '@', 1)] - if name.startswith('--'): - return [name.replace('--', '@', 1)] - return [name.replace('-', '@', 1)] - - def add_action_parser(self, name, api=None, **kwargs): - """Add a parser for an action - - Keyword arguments: - - api -- The action route (e.g. 'GET /' ) - - Returns: - A new _HTTPArgumentParser object for the route - - """ - if not api: - return None - - # Validate action route - m = re.match('(GET|POST|PUT|DELETE) (/\S+)', api) - if not m: - return None - - # Check if a parser already exists for the route - key = (m.group(1), m.group(2)) - if key in self.routes: - raise ValueError("A parser for '%s' already exists" % key) - - # Create and append parser - parser = _HTTPArgumentParser() - self._parsers[key] = parser - - # Return the created parser - return parser - - def parse_args(self, args, route, **kwargs): - """Parse arguments - - Keyword arguments: - - route -- The action route (e.g. 'GET /' ) - - """ - # Retrieve the parser for the route - if route not in self.routes: - raise MoulinetteError(22, "No parser for '%s %s' found" % key) - - return self._parsers[route].parse_args(args) - -actionsmap_parser = APIParser - - -## API moulinette interface +# API moulinette interface --------------------------------------------- class _APIAuthPlugin(object): """ @@ -189,7 +41,7 @@ class _APIAuthPlugin(object): session_opts = { 'session.type': 'file', 'session.cookie_expires': True, - 'session.data_dir': pkg.cachedir('session', make_dir=True), + 'session.data_dir': pkg.get_cachedir('session'), 'session.secure': True } self._app = SessionMiddleware(app, session_opts) @@ -361,11 +213,11 @@ class MoulinetteAPI(object): """ if category is None: - with open(pkg.datafile('doc/resources.json')) as f: + with open('%s/doc/resources.json' % pkg.datadir) as f: return f.read() try: - with open(pkg.datafile('doc/%s.json' % category)) as f: + with open('%s/doc/%s.json' % (pkg.datadir, category)) as f: return f.read() except IOError: return 'unknown' diff --git a/src/moulinette/interface/cli.py b/src/moulinette/interface/cli.py index c4be4c57..e10b0a25 100644 --- a/src/moulinette/interface/cli.py +++ b/src/moulinette/interface/cli.py @@ -1,35 +1,5 @@ # -*- coding: utf-8 -*- -import argparse -from . import BaseParser - -## CLI arguments Parser - -class CLIParser(BaseParser): - """Actions map's CLI Parser - - """ - def __init__(self, parser=None): - self._parser = parser or argparse.ArgumentParser() - self._subparsers = self._parser.add_subparsers() - - @staticmethod - def format_arg_name(name, full): - if name[0] == '-' and full: - return [name, full] - return [name] - - def add_general_parser(self, **kwargs): - return self._parser - - def add_category_parser(self, name, category_help=None, **kwargs): - parser = self._subparsers.add_parser(name, help=category_help) - return CLIParser(parser) - - def add_action_parser(self, name, action_help, **kwargs): - return self._subparsers.add_parser(name, help=action_help) - - def parse_args(self, args, **kwargs): - return self._parser.parse_args(args) - -actionsmap_parser = CLIParser +class MoulinetteCLI(object): + # TODO: Implement this class + pass diff --git a/src/moulinette/package.py b/src/moulinette/package.py new file mode 100644 index 00000000..e4326da3 --- /dev/null +++ b/src/moulinette/package.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- + +# Public constants defined during build + +"""Package's data directory (e.g. /usr/share/moulinette)""" +datadir = '/usr/share/moulinette' + +"""Package's library directory (e.g. /usr/lib/moulinette)""" +libdir = '/usr/lib/moulinette' + +"""Locale directory for the package (e.g. /usr/lib/moulinette/locale)""" +localedir = '/usr/lib/moulinette/locale' + +"""Cache directory for the package (e.g. /var/cache/moulinette)""" +cachedir = '/var/cache/moulinette' diff --git a/src/moulinette/package.py.in b/src/moulinette/package.py.in new file mode 100644 index 00000000..a72b5486 --- /dev/null +++ b/src/moulinette/package.py.in @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- + +# Public constants defined during build + +"""Package's data directory (e.g. /usr/share/moulinette)""" +datadir = %PKGDATADIR% + +"""Package's library directory (e.g. /usr/lib/moulinette)""" +libdir = %PKGLIBDIR% + +"""Locale directory for the package (e.g. /usr/lib/moulinette/locale)""" +localedir = %PKGLOCALEDIR% + +"""Cache directory for the package (e.g. /var/cache/moulinette)""" +cachedir = %PKGCACHEDIR% diff --git a/src/yunohost/__init__.py b/src/yunohost/__init__.py deleted file mode 100755 index e69de29b..00000000