Refactore interfaces and reorganize files a bit

* Remove 'core' folder and reorganize root package structure
* Introduce interface's base class and implement 'api' and 'cli'
* Add a Package class and a moulinette initialization method
* Start to replace YunoHostError by MoulinetteError
* Fix actionsmap/yunohost.yml to follow extra parameters rules
This commit is contained in:
Jerome Lebleu 2014-03-04 12:31:04 +01:00
parent 2b370be233
commit 9104024fa1
24 changed files with 931 additions and 751 deletions

View file

@ -5,25 +5,29 @@ import sys
import os.path
import gettext
# Debug option
if '--debug' in sys.argv:
sys.path.append(os.path.abspath(os.path.dirname(__file__) +'/../src'))
from moulinette import cli
from moulinette.core.helpers import YunoHostError, colorize
# Run from source
basedir = os.path.abspath(os.path.dirname(__file__) +'/../')
if os.path.isdir(basedir +'/src'):
sys.path.append(basedir +'/src')
gettext.install('YunoHost')
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'))
# Additional arguments
use_cache = True
if '--no-cache' in sys.argv:
use_cache = False
sys.argv.remove('--no-cache')
if '--debug' in sys.argv:
sys.argv.remove('--debug')
try:
args = list(sys.argv)
@ -36,6 +40,9 @@ if __name__ == '__main__':
# Execute the action
cli(args, use_cache)
except MoulinetteError as e:
print(e.colorize())
sys.exit(e.code)
except YunoHostError as e:
print(colorize(_("Error: "), 'red') + e.message)
sys.exit(e.code)

View file

@ -5,12 +5,14 @@ import sys
import os.path
import gettext
# Debug option
if '--debug' in sys.argv:
sys.path.append(os.path.abspath(os.path.dirname(__file__) +'/../src'))
from moulinette import api
# Run from source
basedir = os.path.abspath(os.path.dirname(__file__) +'/../')
if os.path.isdir(basedir +'/src'):
sys.path.append(basedir +'/src')
gettext.install('YunoHost')
from moulinette import init, api
gettext.install('yunohost')
## Callbacks for additional routes
@ -29,6 +31,10 @@ 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'))
# Additional arguments
use_cache = True
if '--no-cache' in sys.argv:

View file

@ -76,7 +76,9 @@ user:
help: Must be unique
extra:
ask: "Username"
pattern: '^[a-z0-9_]+$'
pattern:
- '^[a-z0-9_]+$'
- "Must be alphanumeric and underscore characters only"
-f:
full: --firstname
extra:
@ -109,7 +111,9 @@ user:
nargs: "*"
extra:
ask: "Users to delete"
pattern: '^[a-z0-9_]+$'
pattern:
- '^[a-z0-9_]+$'
- "Must be alphanumeric and underscore characters only"
--purge:
action: store_true
@ -187,7 +191,9 @@ domain:
help: Domain name to add
nargs: '+'
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])*)$'
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])*)$'
- "Must be a valid domain name (e.g. my-domain.org)"
-m:
full: --main
help: Is the main domain
@ -206,7 +212,9 @@ domain:
help: Domain(s) to delete
nargs: "+"
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])*)$'
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])*)$'
- "Must be a valid domain name (e.g. my-domain.org)"
### domain_info()
info:
@ -216,7 +224,9 @@ domain:
domain:
help: ""
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])*)$'
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])*)$'
- "Must be a valid domain name (e.g. my-domain.org)"
#############################
@ -253,7 +263,9 @@ app:
help: Name of the list to remove
extra:
ask: "List to remove"
pattern: '^[a-z0-9_]+$'
pattern:
- '^[a-z0-9_]+$'
- "Must be alphanumeric and underscore characters only"
### app_list()
list:
@ -302,7 +314,9 @@ app:
full: --user
help: Allowed app map for a user
extra:
pattern: '^[a-z0-9_]+$'
pattern:
- '^[a-z0-9_]+$'
- "Must be alphanumeric and underscore characters only"
### app_install() TODO: Write help
@ -388,7 +402,9 @@ app:
port:
help: Port to check
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])$'
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])$'
- "Must be a valid port number (i.e. 0-65535)"
### app_checkurl()
checkurl:
@ -659,7 +675,9 @@ service:
help: Number of lines to display
default: "50"
extra:
pattern: '^[0-9]+$'
pattern:
- '^[0-9]+$'
- "Must be a valid number"
#############################
@ -691,7 +709,9 @@ firewall:
port:
help: Port to open
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])$'
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])$'
- "Must be a valid port number (i.e. 0-65535)"
protocol:
help: Protocol associated with port
choices:
@ -840,12 +860,16 @@ tools:
-o:
full: --old-domain
extra:
pattern: '^([a-zA-Z0-9]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)(\.[a-zA-Z0-9]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)*(\.[a-zA-Z]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)$'
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])*)$'
- "Must be a valid domain name (e.g. my-domain.org)"
-n:
full: --new-domain
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])*)$'
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])*)$'
- "Must be a valid domain name (e.g. my-domain.org)"
### tools_postinstall()
postinstall:
@ -857,7 +881,9 @@ tools:
help: YunoHost main domain
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])*)$'
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])*)$'
- "Must be a valid domain name (e.g. my-domain.org)"
-p:
full: --password
help: YunoHost admin password

View file

@ -24,68 +24,98 @@ __credits__ = """
You should have received a copy of the GNU Affero General Public License
along with this program; if not, see http://www.gnu.org/licenses
"""
__all__ = [
'init', 'api', 'cli',
'MoulinetteError',
]
from .core import MoulinetteError
curr_namespace = None
## Fast access functions
## Package functions
def init(namespace=None, **kwargs):
"""Package initialization
Initialize directories and global variables. It must be called
before any of package method is used - even the easy access
functions.
Keyword arguments:
- namespace -- The namespace to initialize and use
- **kwargs -- See helpers.Package
At the end, the global variable 'pkg' will contain a Package
instance. See helpers.Package for available methods and variables.
"""
import __builtin__
from .core import Package
global curr_namespace
curr_namespace = namespace
__builtin__.__dict__['pkg'] = Package(**kwargs)
## Easy access to interfaces
def api(port, routes={}, use_cache=True):
"""
"""Web server (API) interface
Run a HTTP server with the moulinette for an API usage.
Keyword arguments:
- port -- Port to run on
- routes -- A dict of additional routes to add in the form of
{(method, uri): callback}
- use_cache -- False if it should parse the actions map file
instead of using the cached one
"""
from bottle import run
from core.actionsmap import ActionsMap
from core.api import MoulinetteAPI
from core.helpers import Interface
from .actionsmap import ActionsMap
from .interface.api import MoulinetteAPI
amap = ActionsMap(Interface.api, use_cache=use_cache)
amap = ActionsMap('api', use_cache=use_cache)
moulinette = MoulinetteAPI(amap, routes)
run(moulinette.app, port=port)
def cli(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:
- args -- A list of argument strings
- use_cache -- False if it should parse the actions map file
instead of using the cached one
"""
import os
from core.actionsmap import ActionsMap
from core.helpers import Interface, YunoHostError, pretty_print_dict
from .actionsmap import ActionsMap
from .helpers import 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"))
raise MoulinetteError(1, _("The moulinette is already running"))
# Create a lock
with open(lock_file, 'w') as f: pass
os.system('chmod 400 '+ lock_file)
try:
amap = ActionsMap(Interface.cli, use_cache=use_cache)
amap = ActionsMap('cli', use_cache=use_cache)
pretty_print_dict(amap.process(args))
except KeyboardInterrupt, EOFError:
raise YunoHostError(125, _("Interrupted"))
raise MoulinetteError(125, _("Interrupted"))
finally:
# Remove the lock
os.remove(lock_file)

View file

@ -0,0 +1,338 @@
# -*- coding: utf-8 -*-
import pickle
import yaml
import re
import os
from collections import OrderedDict
import logging
from . import __version__, curr_namespace, MoulinetteError
from .extra.parameters import extraparameters_list
## Extra parameters Parser
class ExtraParser(object):
"""
Global parser for the extra parameters.
"""
def __init__(self, iface):
self.iface = iface
self.extra = OrderedDict()
# Append available extra parameters for the current interface
for klass in extraparameters_list:
if iface in klass.skipped_iface:
continue
self.extra[klass.name] = klass
def validate(self, arg_name, parameters):
"""
Validate values of extra parameters for an argument
Keyword arguments:
- arg_name -- The argument name
- parameters -- A dict of extra parameters with their values
"""
# Iterate over parameters to validate
for p, v in parameters.items():
# Remove unknow parameters
if p not in self.extra.keys():
del parameters[p]
# Validate parameter value
parameters[p] = self.extra[p].validate(v, arg_name)
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:
arg_value = parser(parameters[p], arg_name, arg_value)
return arg_value
## Main class
class ActionsMap(object):
"""
Validate and process action defined into the actions map.
The actions map defines 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:
- interface -- Interface type that requires the actions map.
Possible value is one of:
- 'cli' for the command line interface
- 'api' for an API usage (HTTP requests)
- use_cache -- False if it should parse the actions map file
instead of using the cached one.
"""
def __init__(self, interface, 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):
raise MoulinetteError(22, _("Invalid interface '%s'" % interface))
else:
self._parser_class = parser
logging.debug("initializing ActionsMap for the '%s' interface" % interface)
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:
actionsmaps[n] = pickle.load(f)
else:
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:
actionsmaps[n] = yaml.load(f)
# Generate parsers
self.extraparser = ExtraParser(interface)
self.parser = self._construct_parser(actionsmaps)
def process(self, args, **kwargs):
"""
Parse arguments and process the proper action
Keyword arguments:
- args -- The arguments to parse
- **kwargs -- Additional interface arguments
"""
# Parse arguments
arguments = vars(self.parser.parse_args(args, **kwargs))
arguments = self._parse_extra_parameters(arguments)
# Retrieve action information
namespace, category, action = arguments.pop('_info')
func_name = '%s_%s' % (category, action)
try:
mod = __import__('%s.%s' % (namespace, category),
globals=globals(), level=0,
fromlist=[func_name])
func = getattr(mod, func_name)
except (AttributeError, ImportError):
raise MoulinetteError(168, _('Function is not defined'))
else:
# Process the action
return func(**arguments)
@staticmethod
def get_actionsmap_namespaces():
"""
Retrieve actions map namespaces from a given path
Returns:
A list of available namespaces
"""
namespaces = []
for f in os.listdir(pkg.datadir('actionsmap')):
if f.endswith('.yml'):
namespaces.append(f[:-4])
return namespaces
@classmethod
def generate_cache(klass, namespaces=None):
"""
Generate cache for the actions map's file(s)
Keyword arguments:
- namespaces -- A list of namespaces to generate cache for
Returns:
A dict of actions map for each namespaces
"""
actionsmaps = {}
if not namespaces:
namespaces = klass.get_actionsmap_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)
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:
pickle.dump(actionsmaps[n], f)
return actionsmaps
## Private class and methods
def _store_extra_parameters(self, parser, arg_name, arg_extra):
"""
Store extra parameters for a given argument
Keyword arguments:
- parser -- Parser object for the arguments
- arg_name -- Argument name
- arg_extra -- Argument extra parameters
Returns:
The parser object
"""
if arg_extra:
# Retrieve current extra parameters dict
extra = parser.get_default('_extra')
if not extra or not isinstance(extra, dict):
extra = {}
if not self.use_cache:
# Validate extra parameters for the argument
extra[arg_name] = self.extraparser.validate(arg_name, arg_extra)
else:
extra[arg_name] = arg_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 for the arguments
extra = args.pop('_extra', None)
if not extra:
return args
# Validate extra parameters for each arguments
for an, parameters in extra.items():
args[an] = self.extraparser.parse(an, args[an], parameters)
return args
def _construct_parser(self, actionsmaps):
"""
Construct the parser with the actions map
Keyword arguments:
- actionsmaps -- A dict of multi-level dictionnary of
categories/actions/arguments list for each namespaces
Returns:
An interface relevant's parser object
"""
# Instantiate parser
top_parser = self._parser_class()
# Iterate over actions map namespaces
for n, actionsmap in actionsmaps.items():
if 'general_arguments' in actionsmap:
# Parse general arguments
if top_parser.parse_general:
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__)
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:
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
# 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))
# Add action arguments
for argn, argp in arguments.items():
name = top_parser.format_arg_name(argn, argp.pop('full', None))
extra = argp.pop('extra', None)
arg = parser.add_argument(*name, **argp)
parser = self._store_extra_parameters(parser, arg.dest, extra)
return top_parser

View file

@ -1,16 +0,0 @@
# -*- coding: utf-8 -*-
# TODO: Remove permanent debug values
import os
# Path for the the web sessions
session_path = '/var/cache/yunohost/session'
# Path of the actions map definition(s)
actionsmap_path = os.path.dirname(__file__) +'/../../data/actionsmap'
# Path for the actions map cache
actionsmap_cache_path = '/var/cache/yunohost/actionsmap'
# Path of the doc in json format
doc_json_path = os.path.dirname(__file__) +'/../../doc'

155
src/moulinette/core.py Normal file
View file

@ -0,0 +1,155 @@
# -*- coding: utf-8 -*-
import os
import sys
import gettext
from .helpers import colorize
class Package(object):
"""Package representation and easy access
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.
"""
def __init__(self, prefix, libdir, cachedir, destdir=None):
if not prefix and not libdir:
# Running from source directory
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
else:
self._datadir = os.path.join(prefix, 'share/moulinette')
self._libdir = os.path.join(libdir, 'moulinette')
self._cachedir = os.path.join(cachedir, 'moulinette')
# Append library path to python's path
sys.path.append(self._libdir)
self._destdir = destdir or None
## Easy access to directories and files
def datadir(self, subdir=None, **kwargs):
"""Return the path to a data directory"""
return self.get_dir(self._datadir, subdir, **kwargs)
def datafile(self, filename, **kwargs):
"""Return the path to a data file"""
return self.get_file(self._datadir, filename, **kwargs)
def libdir(self, subdir=None, **kwargs):
"""Return the path to a lib directory"""
return self.get_dir(self._libdir, subdir, **kwargs)
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)
## Standard methods
def get_dir(self, basedir, subdir=None, make_dir=False):
"""Get a directory path
Return a path composed by a base directory and an optional
subdirectory. The path will be created if needed.
Keyword arguments:
- basedir -- The base directory
- subdir -- An optional subdirectory
- make_dir -- True if it should create needed directory
"""
# Retrieve path
path = basedir
if self._destdir:
path = os.path.join(self._destdir, path)
if subdir:
path = os.path.join(path, 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
Return the path of the filename in the specified directory. This
directory will be created if needed.
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
"""
# Check for a directory in filename
subdir = os.path.dirname(filename) or None
if subdir:
filename = os.path.basename(filename)
# Get directory path
dirpath = self.get_dir(basedir, subdir, **kwargs)
return os.path.join(dirpath, filename)
class MoulinetteError(Exception):
"""Moulinette base exception
Keyword arguments:
- code -- Integer error code
- message -- Error message to display
"""
def __init__(self, code, message):
self.code = code
self.message = message
errorcode_desc = {
1 : _('Fail'),
13 : _('Permission denied'),
17 : _('Already exists'),
22 : _('Invalid arguments'),
87 : _('Too many users'),
111 : _('Connection refused'),
122 : _('Quota exceeded'),
125 : _('Operation canceled'),
167 : _('Not found'),
168 : _('Undefined'),
169 : _('LDAP operation error')
}
if code in errorcode_desc:
self.desc = errorcode_desc[code]
else:
self.desc = _('Error %s' % code)
def __str__(self, colorized=False):
desc = self.desc
if colorized:
desc = colorize(self.desc, 'red')
return _('%s: %s' % (desc, self.message))
def colorize(self):
return self.__str__(colorized=True)

View file

@ -1,503 +0,0 @@
# -*- coding: utf-8 -*-
import argparse
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 extraparameters import extraparameters_list
from helpers import Interface, YunoHostError
## Additional parsers
class _HTTPArgumentParser(object):
def __init__(self, method, uri):
# Initialize the ArgumentParser object
self._parser = argparse.ArgumentParser(usage='',
prefix_chars='@',
add_help=False)
self._parser.error = self._error
self.method = method
self.uri = uri
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 Exception(message)
class HTTPParser(object):
"""
Object for parsing HTTP requests into Python objects.
"""
def __init__(self):
self._parsers = {} # dict({(method, uri): _HTTPArgumentParser})
@property
def routes(self):
"""Get current routes"""
return self._parsers.keys()
def add_parser(self, method, uri):
"""
Add a parser for a given route
Keyword arguments:
- method -- The route's HTTP method (GET, POST, PUT, DELETE)
- uri -- The route's URI
Returns:
A new _HTTPArgumentParser object for the route
"""
# Check if a parser already exists for the route
key = (method, uri)
if key in self.routes:
raise ValueError("A parser for '%s' already exists" % key)
# Create and append parser
parser = _HTTPArgumentParser(method, uri)
self._parsers[key] = parser
# Return the created parser
return parser
def parse_args(self, method, uri, args={}):
"""
Convert argument variables to objects and assign them as
attributes of the namespace for a given route
Keyword arguments:
- method -- The route's HTTP method (GET, POST, PUT, DELETE)
- uri -- The route's URI
- args -- Argument variables for the route
Returns:
The populated namespace
"""
# Retrieve the parser for the route
key = (method, uri)
if key not in self.routes:
raise ValueError("No parser for '%s %s' found" % key)
return self._parsers[key].parse_args(args)
class ExtraParser(object):
"""
Global parser for the extra parameters.
"""
def __init__(self, iface):
self.iface = iface
self.extra = OrderedDict()
# 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 validate(self, arg_name, parameters):
"""
Validate values of extra parameters for an argument
Keyword arguments:
- arg_name -- The argument name
- parameters -- A dict of extra parameters with their values
"""
# Iterate over parameters to validate
for p, v in parameters.items():
# Remove unknow parameters
if p not in self.extra.keys():
del parameters[p]
# Validate parameter value
parameters[p] = self.extra[p].validate(v, arg_name)
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:
arg_value = parser(parameters[p], arg_name, arg_value)
return arg_value
## Main class
class ActionsMap(object):
"""
Validate and process action defined into the actions map.
The actions map defines 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:
- interface -- Interface type that requires the actions map.
Possible value is one of:
- 'cli' for the command line interface
- 'api' for an API usage (HTTP requests)
- use_cache -- False if it should parse the actions map file
instead of using the cached one.
"""
def __init__(self, interface, use_cache=True):
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
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:
actionsmaps[n] = pickle.load(f)
else:
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:
actionsmaps[n] = yaml.load(f)
# Generate parsers
self.extraparser = ExtraParser(interface)
self.parser = self._construct_parser(actionsmaps)
def process(self, args, route=None):
"""
Parse arguments and process the proper action
Keyword arguments:
- args -- The arguments to parse
- route -- A tupple (method, uri) of the requested route (api only)
"""
arguments = None
# Parse arguments
if self.interface == Interface.cli:
arguments = self.parser.parse_args(args)
elif self.interface == Interface.api:
if route is None:
# TODO: Raise a proper exception
raise Exception(_("Missing route argument"))
method, uri = route
arguments = self.parser.parse_args(method, uri, args)
arguments = vars(arguments)
# Parse extra parameters
arguments = self._parse_extra_parameters(arguments)
# Retrieve action information
namespace = arguments['_info']['namespace']
category = arguments['_info']['category']
action = arguments['_info']['action']
del arguments['_info']
module = '%s.%s' % (namespace, category)
function = '%s_%s' % (category, action)
try:
mod = __import__(module, globals=globals(), fromlist=[function], level=0)
func = getattr(mod, function)
except (AttributeError, ImportError):
raise YunoHostError(168, _('Function is not defined'))
else:
# Process the action
return func(**arguments)
@staticmethod
def get_actionsmap_namespaces(path=actionsmap_path):
"""
Retrieve actions map namespaces from a given path
Returns:
A list of available namespaces
"""
namespaces = []
for f in os.listdir(path):
if f.endswith('.yml'):
namespaces.append(f[:-4])
return namespaces
@classmethod
def generate_cache(klass):
"""
Generate cache for the actions map's file(s)
Returns:
A dict of actions map for each namespaces
"""
actionsmaps = {}
if not os.path.isdir(actionsmap_cache_path):
os.makedirs(actionsmap_cache_path)
# 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:
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(actionsmaps[n], f)
return actionsmaps
## Private class and methods
def _store_extra_parameters(self, parser, arg_name, arg_params):
"""
Store extra parameters for a given argument
Keyword arguments:
- parser -- Parser object for the arguments
- arg_name -- Argument name
- arg_params -- Argument parameters
Returns:
The parser object
"""
if 'extra' in arg_params:
# Retrieve current extra parameters dict
extra = parser.get_default('_extra')
if not extra or not isinstance(extra, dict):
extra = {}
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
extra = args['_extra']
del args['_extra']
# Validate extra parameters for each arguments
for an, parameters in extra.items():
args[an] = self.extraparser.parse(an, args[an], parameters)
return args
def _construct_parser(self, actionsmaps):
"""
Construct the parser with the actions map
Keyword arguments:
- actionsmaps -- A dict of multi-level dictionnary of
categories/actions/arguments list for each namespaces
Returns:
An interface relevant's parser object
"""
top_parser = None
iface = self.interface
# Create parser object
if iface == Interface.cli:
# TODO: Add descritpion (from __description__?)
top_parser = argparse.ArgumentParser()
top_subparsers = top_parser.add_subparsers()
elif iface == Interface.api:
top_parser = HTTPParser()
## Format option strings from argument parameters
def _option_strings(arg_name, arg_params):
if iface == Interface.cli:
if arg_name[0] == '-' and 'full' in arg_params:
return [arg_name, arg_params['full']]
return [arg_name]
elif iface == Interface.api:
if arg_name[0] != '-':
return [arg_name]
if 'full' in arg_params:
return [arg_params['full'].replace('--', '@', 1)]
if arg_name.startswith('--'):
return [arg_name.replace('--', '@', 1)]
return [arg_name.replace('-', '@', 1)]
## Remove extra parameters
def _clean_params(arg_params):
for k in {'full', 'extra'}:
if k in arg_params:
del arg_params[k]
return arg_params
# Iterate over actions map namespaces
for n, actionsmap in actionsmaps.items():
# Parse general arguments for the cli only
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['general_arguments']
# Parse categories
for cn, cp in actionsmap.items():
if 'actions' not in cp:
continue
# Add category subparsers for the cli only
if iface == Interface.cli:
c_help = cp.get('category_help')
subparsers = top_subparsers.add_parser(cn, help=c_help).add_subparsers()
# Parse actions
for an, ap in cp['actions'].items():
parser = None
# Add parser for the current action
if iface == Interface.cli:
a_help = ap.get('action_help')
parser = subparsers.add_parser(an, help=a_help)
elif iface == Interface.api and 'api' in ap:
# Extract method and uri
m = re.match('(GET|POST|PUT|DELETE) (/\S+)', ap['api'])
if m:
parser = top_parser.add_parser(m.group(1), m.group(2))
if not parser:
continue
# Store action information
parser.set_defaults(_info={'namespace': n,
'category': cn,
'action': an})
# Add arguments
if not 'arguments' in ap:
continue
for argn, argp in ap['arguments'].items():
arg = parser.add_argument(*_option_strings(argn, argp),
**_clean_params(argp.copy()))
parser = self._store_extra_parameters(parser, arg.dest, argp)
return top_parser

View file

View file

@ -4,7 +4,8 @@ import getpass
import re
import logging
from helpers import Interface, colorize, YunoHostError
from .. import MoulinetteError
from ..helpers import colorize
class _ExtraParameter(object):
"""
@ -78,7 +79,7 @@ class AskParameter(_ExtraParameter):
"""
name = 'ask'
skipped_iface = {Interface.api}
skipped_iface = { 'api' }
def __call__(self, message, arg_name, arg_value):
# TODO: Fix asked arguments ordering
@ -119,7 +120,7 @@ class PasswordParameter(AskParameter):
pwd1 = getpass.getpass(colorize(message + ': ', 'cyan'))
pwd2 = getpass.getpass(colorize('Retype ' + message + ': ', 'cyan'))
if pwd1 != pwd2:
raise YunoHostError(22, _("Passwords don't match"))
raise MoulinetteError(22, _("Passwords don't match"))
return pwd1
class PatternParameter(_ExtraParameter):
@ -137,7 +138,7 @@ class PatternParameter(_ExtraParameter):
message = arguments[1]
if arg_value is not None and not re.match(pattern, arg_value):
raise YunoHostError(22, message)
raise MoulinetteError(22, message)
return arg_value
@staticmethod

View file

@ -21,21 +21,6 @@ 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):
@ -105,33 +90,6 @@ def win_msg(astr):
win.append(astr)
def str_to_func(astr):
"""
Call a function from a string name
Keyword arguments:
astr -- Name of function to call
Returns:
Function
"""
try:
module, _, function = astr.rpartition('.')
if module:
__import__(module)
mod = sys.modules[module]
else:
mod = sys.modules['__main__'] # default module
func = getattr(mod, function)
except (AttributeError, ImportError):
#raise YunoHostError(168, _('Function is not defined'))
return None
else:
return func
def validate(pattern, array):
"""
Validate attributes with a pattern
@ -441,115 +399,3 @@ class YunoHostLDAP(Singleton):
else:
raise YunoHostError(17, _('Attribute already exists') + ' "' + attr + '=' + value + '"')
return True
def parse_dict(action_map):
"""
Turn action dictionnary to parser, subparsers and arguments
Keyword arguments:
action_map -- Multi-level dictionnary of categories/actions/arguments list
Returns:
Namespace of args
"""
# Intialize parsers
parsers = subparsers_category = subparsers_action = {}
parsers['general'] = argparse.ArgumentParser()
subparsers = parsers['general'].add_subparsers()
new_args = []
patterns = {}
# Add general arguments
for arg_name, arg_params in action_map['general_arguments'].items():
if 'version' in arg_params:
v = arg_params['version']
arg_params['version'] = v.replace('%version%', __version__)
if 'full' in arg_params:
arg_names = [arg_name, arg_params['full']]
arg_fullname = arg_params['full']
del arg_params['full']
else: arg_names = [arg_name]
parsers['general'].add_argument(*arg_names, **arg_params)
del action_map['general_arguments']
# Split categories into subparsers
for category, category_params in action_map.items():
if 'category_help' not in category_params: category_params['category_help'] = ''
subparsers_category[category] = subparsers.add_parser(category, help=category_params['category_help'])
subparsers_action[category] = subparsers_category[category].add_subparsers()
# Split actions
if 'actions' in category_params:
for action, action_params in category_params['actions'].items():
if 'action_help' not in action_params: action_params['action_help'] = ''
parsers[category + '_' + action] = subparsers_action[category].add_parser(action, help=action_params['action_help'])
# Set the action s related function
parsers[category + '_' + action].set_defaults(
func=str_to_func('yunohost_' + category + '.'
+ category + '_' + action.replace('-', '_')))
# Add arguments
if 'arguments' in action_params:
for arg_name, arg_params in action_params['arguments'].items():
arg_fullname = False
if 'password' in arg_params:
if arg_params['password']: is_password = True
del arg_params['password']
else: is_password = False
if 'full' in arg_params:
arg_names = [arg_name, arg_params['full']]
arg_fullname = arg_params['full']
del arg_params['full']
else: arg_names = [arg_name]
if 'ask' in arg_params:
require_input = True
if '-h' in sys.argv or '--help' in sys.argv:
require_input = False
if (category != sys.argv[1]) or (action != sys.argv[2]):
require_input = False
for name in arg_names:
if name in sys.argv[2:]: require_input = False
if require_input:
if is_password:
if os.isatty(1):
pwd1 = getpass.getpass(colorize(arg_params['ask'] + ': ', 'cyan'))
pwd2 = getpass.getpass(colorize('Retype ' + arg_params['ask'][0].lower() + arg_params['ask'][1:] + ': ', 'cyan'))
if pwd1 != pwd2:
raise YunoHostError(22, _("Passwords don't match"))
sys.exit(1)
else:
raise YunoHostError(22, _("Missing arguments") + ': ' + arg_name)
if arg_name[0] == '-': arg_extend = [arg_name, pwd1]
else: arg_extend = [pwd1]
else:
if os.isatty(1):
arg_value = raw_input(colorize(arg_params['ask'] + ': ', 'cyan'))
else:
raise YunoHostError(22, _("Missing arguments") + ': ' + arg_name)
if arg_name[0] == '-': arg_extend = [arg_name, arg_value]
else: arg_extend = [arg_value]
new_args.extend(arg_extend)
del arg_params['ask']
if 'pattern' in arg_params:
if (category == sys.argv[1]) and (action == sys.argv[2]):
if 'dest' in arg_params: name = arg_params['dest']
elif arg_fullname: name = arg_fullname[2:]
else: name = arg_name
name = name.replace('-', '_')
patterns[name] = arg_params['pattern']
del arg_params['pattern']
parsers[category + '_' + action].add_argument(*arg_names, **arg_params)
args = parsers['general'].parse_args(sys.argv.extend(new_args))
args_dict = vars(args)
for key, value in patterns.items():
validate(value, args_dict[key])
return args

View file

@ -0,0 +1,114 @@
# -*- 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__)

View file

@ -1,16 +1,162 @@
# -*- coding: utf-8 -*-
import re
import argparse
import os.path
from bottle import Bottle, request, response, HTTPResponse
from beaker.middleware import SessionMiddleware
from ..config import session_path, doc_json_path
from helpers import YunoHostError, YunoHostLDAP
from . import BaseParser
from .. 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()
## Bottle Plugins
## Implement virtual methods
class APIAuthPlugin(object):
@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
class _APIAuthPlugin(object):
"""
Manage the authentication for the API access.
@ -20,8 +166,7 @@ class APIAuthPlugin(object):
def __init__(self):
# TODO: Add options (e.g. session type, content type, ...)
if not os.path.isdir(session_path):
os.makedirs(session_path)
pass
@property
def app(self):
@ -44,7 +189,7 @@ class APIAuthPlugin(object):
session_opts = {
'session.type': 'file',
'session.cookie_expires': True,
'session.data_dir': session_path,
'session.data_dir': pkg.cachedir('session', make_dir=True),
'session.secure': True
}
self._app = SessionMiddleware(app, session_opts)
@ -119,7 +264,7 @@ class APIAuthPlugin(object):
return True
return False
class ActionsMapPlugin(object):
class _ActionsMapPlugin(object):
"""
Process action for the request using the actions map.
@ -158,19 +303,15 @@ class ActionsMapPlugin(object):
return wrapper
## Main class
class MoulinetteAPI(object):
"""
Initialize a HTTP server which serves the API to access to the
moulinette actions.
Keyword arguments:
- actionsmap -- The relevant ActionsMap instance
- routes -- A dict of additional routes to add in the form of
{(method, uri): callback}
{(method, path): callback}
"""
@ -182,14 +323,14 @@ class MoulinetteAPI(object):
callback=self.doc, skip=['apiauth'])
# Append routes from the actions map
amap = ActionsMapPlugin(actionsmap)
for (m, u) in actionsmap.parser.routes:
app.route(u, method=m, callback=self._error, apply=amap)
amap = _ActionsMapPlugin(actionsmap)
for (m, p) in actionsmap.parser.routes:
app.route(p, method=m, callback=self._error, apply=amap)
# Append additional routes
# TODO: Add an option to skip auth for the route
for (m, u), c in routes.items():
app.route(u, method=m, callback=c)
for (m, p), c in routes.items():
app.route(p, method=m, callback=c)
# Define and install a plugin which sets proper header
def apiheader(callback):
@ -201,7 +342,7 @@ class MoulinetteAPI(object):
app.install(apiheader)
# Install authentication plugin
apiauth = APIAuthPlugin()
apiauth = _APIAuthPlugin()
app.install(apiauth)
self._app = apiauth.app
@ -220,11 +361,11 @@ class MoulinetteAPI(object):
"""
if category is None:
with open(doc_json_path +'/resources.json') as f:
with open(pkg.datafile('doc/resources.json')) as f:
return f.read()
try:
with open(doc_json_path +'/'+ category +'.json') as f:
with open(pkg.datafile('doc/%s.json' % category)) as f:
return f.read()
except IOError:
return 'unknown'

View file

@ -0,0 +1,35 @@
# -*- 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

View file

@ -37,7 +37,7 @@ from domain import domain_list, domain_add
from user import user_info, user_list
from hook import hook_exec, hook_add, hook_remove
from .moulinette.core.helpers import YunoHostError, YunoHostLDAP, win_msg, random_password, is_true, validate
from moulinette.helpers import YunoHostError, YunoHostLDAP, win_msg, random_password, is_true, validate
repo_path = '/var/cache/yunohost/repo'
apps_path = '/usr/share/yunohost/apps'

View file

@ -29,7 +29,7 @@ import json
import yaml
import glob
from moulinette.core.helpers import YunoHostError, YunoHostLDAP, validate, colorize, win_msg
from moulinette.helpers import YunoHostError, YunoHostLDAP, validate, colorize, win_msg
def backup_init(helper=False):
"""

View file

@ -34,7 +34,7 @@ import requests
from urllib import urlopen
from dyndns import dyndns_subscribe
from moulinette.core.helpers import YunoHostError, YunoHostLDAP, win_msg, colorize, validate, get_required_args
from moulinette.helpers import YunoHostError, YunoHostLDAP, win_msg, colorize, validate, get_required_args
def domain_list(filter=None, limit=None, offset=None):

View file

@ -30,7 +30,7 @@ import json
import glob
import base64
from moulinette.core.helpers import YunoHostError, YunoHostLDAP, validate, colorize, win_msg
from moulinette.helpers import YunoHostError, YunoHostLDAP, validate, colorize, win_msg
def dyndns_subscribe(subscribe_host="dyndns.yunohost.org", domain=None, key=None):
"""

View file

@ -37,7 +37,7 @@ except ImportError:
sys.stderr.write('apt-get install python-yaml\n')
sys.exit(1)
from moulinette.core.helpers import YunoHostError, win_msg
from moulinette.helpers import YunoHostError, win_msg
def firewall_allow(protocol=None, port=None, ipv6=None, upnp=False):

View file

@ -28,7 +28,7 @@ import sys
import re
import json
from moulinette.core.helpers import YunoHostError, YunoHostLDAP, win_msg, colorize
from moulinette.helpers import YunoHostError, YunoHostLDAP, win_msg, colorize
hook_folder = '/usr/share/yunohost/hooks/'

View file

@ -37,7 +37,7 @@ from datetime import datetime, timedelta
from service import (service_enable, service_disable,
service_start, service_stop, service_status)
from moulinette.core.helpers import YunoHostError, win_msg
from moulinette.helpers import YunoHostError, win_msg
glances_uri = 'http://127.0.0.1:61209'
stats_path = '/var/lib/yunohost/stats'

View file

@ -28,7 +28,7 @@ import glob
import subprocess
import os.path
from moulinette.core.helpers import YunoHostError, win_msg
from moulinette.helpers import YunoHostError, win_msg
def service_start(names):

View file

@ -36,7 +36,7 @@ from dyndns import dyndns_subscribe
from backup import backup_init
from app import app_ssowatconf
from moulinette.core.helpers import YunoHostError, YunoHostLDAP, validate, colorize, get_required_args, win_msg
from moulinette.helpers import YunoHostError, YunoHostLDAP, validate, colorize, get_required_args, win_msg
def tools_ldapinit(password=None):

View file

@ -33,7 +33,7 @@ import getpass
from domain import domain_list
from hook import hook_callback
from moulinette.core.helpers import YunoHostError, YunoHostLDAP, win_msg, colorize, validate, get_required_args
from moulinette.helpers import YunoHostError, YunoHostLDAP, win_msg, colorize, validate, get_required_args
def user_list(fields=None, filter=None, limit=None, offset=None):
"""