Initial refactoring

This commit is contained in:
Jerome Lebleu 2014-02-05 02:01:03 +01:00
parent aa1c87c4d0
commit a802bcfd6f
31 changed files with 941 additions and 527 deletions

42
bin/yunohost Executable file
View file

@ -0,0 +1,42 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
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
gettext.install('YunoHost')
## Main action
if __name__ == '__main__':
# 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)
args.pop(0)
# Check that YunoHost is installed
if not os.path.isfile('/etc/yunohost/installed') \
and (len(args) < 2 or args[1] != 'tools' or args[2] != 'postinstall'):
raise YunoHostError(17, _("YunoHost is not correctly installed, please execute 'yunohost tools postinstall'"))
# Execute the action
cli(args, use_cache)
except YunoHostError as e:
print(colorize(_("Error: "), 'red') + e.message)
sys.exit(e.code)
sys.exit(0)

43
bin/yunohost-api Executable file
View file

@ -0,0 +1,43 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
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
gettext.install('YunoHost')
## Callbacks for additional routes
def is_installed():
"""
Check whether YunoHost is installed or not
"""
installed = False
if os.path.isfile('/etc/yunohost/installed'):
installed = True
return { 'installed': installed }
## Main action
if __name__ == '__main__':
# 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')
# TODO: Add log argument
# Rune the server
api(6787, {('GET', '/installed'): is_installed}, use_cache)
sys.exit(0)

View file

@ -95,7 +95,7 @@ user:
### user_delete()
delete:
action_help: Delete user
api: 'DELETE /users/{users}'
api: 'DELETE /users/<users>'
arguments:
-u:
full: --users
@ -109,7 +109,7 @@ user:
### user_update()
update:
action_help: Update user informations
api: 'PUT /users/{username}'
api: 'PUT /users/<username>'
arguments:
username:
help: Username of user to update
@ -143,7 +143,7 @@ user:
### user_info()
info:
action_help: Get user informations
api: 'GET /users/{username}'
api: 'GET /users/<username>'
arguments:
username:
help: Username or mail to get informations
@ -202,7 +202,7 @@ domain:
### domain_info()
info:
action_help: Get domain informations
api: 'GET /domains/{domain}'
api: 'GET /domains/<domain>'
arguments:
domain:
help: ""
@ -266,7 +266,7 @@ app:
### app_info()
info:
action_help: Get app info
api: GET /app/{app}
api: GET /app/<app>
arguments:
app:
help: Specific app ID
@ -310,7 +310,7 @@ app:
### app_remove() TODO: Write help
remove:
action_help: Remove app
api: DELETE /app/{app}
api: DELETE /app/<app>
arguments:
app:
help: App(s) to delete
@ -333,7 +333,7 @@ app:
### app_setting()
setting:
action_help: Set ou get an app setting value
api: GET /app/{app}/setting
api: GET /app/<app>/setting
arguments:
app:
help: App ID
@ -350,7 +350,7 @@ app:
### app_service()
service:
action_help: Add or remove a YunoHost monitored service
api: POST /app/service/{service}
api: POST /app/service/<service>
arguments:
service:
help: Service to add/remove

View file

@ -1,6 +1,6 @@
UPNP:
UPNP:
cron: false
ports:
ports:
TCP: [22, 25, 53, 80, 443, 465, 993, 5222, 5269, 5280, 6767, 7676]
UDP: [53, 137, 138]
ipv4:

View file

@ -7,14 +7,14 @@ COMPREPLY=()
argc=${COMP_CWORD}
cur="${COMP_WORDS[argc]}"
prev="${COMP_WORDS[argc-1]}"
prev="${COMP_WORDS[argc-1]}"
opts=$(yunohost -h | sed -n "/usage/,/}/p" | awk -F"{" '{print $2}' | awk -F"}" '{print $1}' | tr ',' ' ')
if [[ $argc = 1 ]];
then
COMPREPLY=( $(compgen -W "$opts --help" -- $cur ) )
fi
if [[ "$prev" != "--help" ]];
then
if [[ $argc = 2 ]];
@ -23,9 +23,9 @@ then
COMPREPLY=( $(compgen -W "$opts2 --help" -- $cur ) )
elif [[ $argc = 3 ]];
then
COMPREPLY=( $(compgen -W "--help" $cur ) )
COMPREPLY=( $(compgen -W "--help" $cur ) )
fi
else
else
COMPREPLY=()
fi

89
src/moulinette/__init__.py Executable file
View file

@ -0,0 +1,89 @@
# -*- coding: utf-8 -*-
__title__ = 'moulinette'
__version__ = '695'
__author__ = ['Kload',
'jlebleu',
'titoko',
'beudbeud',
'npze']
__license__ = 'AGPL 3.0'
__credits__ = """
Copyright (C) 2014 YUNOHOST.ORG
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
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
"""
## Fast access functions
def api(port, routes={}, use_cache=True):
"""
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
amap = ActionsMap(ActionsMap.IFACE_API, use_cache=use_cache)
moulinette = MoulinetteAPI(amap, routes)
run(moulinette.app, port=port)
def cli(args, use_cache=True):
"""
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 YunoHostError, pretty_print_dict
lock_file = '/var/run/moulinette.lock'
# Check the lock
if os.path.isfile(lock_file):
raise YunoHostError(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(ActionsMap.IFACE_CLI, use_cache=use_cache)
pretty_print_dict(amap.process(args))
except KeyboardInterrupt, EOFError:
raise YunoHostError(125, _("Interrupted"))
finally:
# Remove the lock
os.remove(lock_file)

16
src/moulinette/config.py Normal file
View file

@ -0,0 +1,16 @@
# -*- 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__) +'/../../etc/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'

View file

View file

@ -0,0 +1,490 @@
# -*- coding: utf-8 -*-
import argparse
import getpass
import marshal
import pickle
import yaml
import re
import os
from .. import __version__
from ..config import actionsmap_path, actionsmap_cache_path
from helpers import YunoHostError, colorize
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 _ExtraParameters(object):
CLI_PARAMETERS = ['ask', 'password', 'pattern']
API_PARAMETERS = ['pattern']
AVAILABLE_PARAMETERS = CLI_PARAMETERS
def __init__(self, **kwargs):
self._params = {}
for k, v in kwargs.items():
if k in self.AVAILABLE_PARAMETERS:
self._params[k] = v
def validate(self, p_name, p_value):
ret = type(p_value)() if p_value is not None else None
for p, v in self._params.items():
func = getattr(self, 'process_' + p)
if isinstance(ret, list):
for p_v in p_value:
r = func(v, p_name, p_v)
if r is not None:
ret.append(r)
else:
r = func(v, p_name, p_value)
if r is not None:
ret = r
return ret
## Parameters validating's method
# TODO: Add doc
def process_ask(self, message, p_name, p_value):
# TODO: Fix asked arguments ordering
if not self._can_prompt(p_value):
return p_value
# Skip password asking
if 'password' in self._params.keys():
return None
ret = raw_input(colorize(message + ': ', 'cyan'))
return ret
def process_password(self, is_password, p_name, p_value):
if not self._can_prompt(p_value):
return p_value
message = self._params['ask']
pwd1 = getpass.getpass(colorize(message + ': ', 'cyan'))
pwd2 = getpass.getpass(colorize('Retype ' + message + ': ', 'cyan'))
if pwd1 != pwd2:
raise YunoHostError(22, _("Passwords don't match"))
return pwd1
def process_pattern(self, pattern, p_name, p_value):
# TODO: Add a pattern_help parameter
# TODO: Fix missing pattern matching on asking
if p_value is not None and not re.match(pattern, p_value):
raise YunoHostError(22, _("'%s' argument not match pattern" % p_name))
return p_value
## Private method
def _can_prompt(self, p_value):
if os.isatty(1) and (p_value is None or p_value == ''):
return True
return False
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.
"""
IFACE_CLI = 'cli'
IFACE_API = 'api'
def __init__(self, interface, use_cache=True):
if interface not in [self.IFACE_CLI,self.IFACE_API]:
raise ValueError(_("Invalid interface '%s'" % interface))
self.interface = interface
# Iterate over actions map namespaces
actionsmap = {}
for n in self.get_actionsmap_namespaces():
if use_cache:
cache_file = '%s/%s.pkl' % (actionsmap_cache_path, n)
if os.path.isfile(cache_file):
with open(cache_file, 'r') as f:
actionsmap[n] = pickle.load(f)
else:
actionsmap = self.generate_cache()
else:
am_file = '%s/%s.yml' % (actionsmap_path, n)
with open(am_file, 'r') as f:
actionsmap[n] = yaml.load(f)
self.parser = self._construct_parser(actionsmap)
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 ==self.IFACE_CLI:
arguments = self.parser.parse_args(args)
elif self.interface ==self.IFACE_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=2)
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 in a given path
"""
namespaces = []
for f in os.listdir(path):
if f.endswith('.yml'):
namespaces.append(f[:-4])
return namespaces
@classmethod
def generate_cache(cls):
"""
Generate cache for the actions map's file(s)
"""
actionsmap = {}
if not os.path.isdir(actionsmap_cache_path):
os.makedirs(actionsmap_cache_path)
for n in cls.get_actionsmap_namespaces():
am_file = '%s/%s.yml' % (actionsmap_path, n)
with open(am_file, 'r') as f:
actionsmap[n] = yaml.load(f)
cache_file = '%s/%s.pkl' % (actionsmap_cache_path, n)
with open(cache_file, 'w') as f:
pickle.dump(actionsmap[n], f)
return actionsmap
## Private class and methods
def _store_extra_parameters(self, parser, arg_name, arg_params):
"""
Store extra parameters for a given parser's argument name
Keyword arguments:
- parser -- Parser object of the argument
- arg_name -- Argument name
- arg_params -- Argument parameters
"""
params = {}
keys = []
# Get available parameters for the current interface
if self.interface ==self.IFACE_CLI:
keys = _ExtraParameters.CLI_PARAMETERS
elif self.interface ==self.IFACE_API:
keys = _ExtraParameters.API_PARAMETERS
for k in keys:
if k in arg_params:
params[k] = arg_params[k]
if len(params) > 0:
# Retrieve all extra parameters from the parser
extra = parser.get_default('_extra')
if not extra or not isinstance(extra, dict):
extra = {}
# Add completed extra parameters to the parser
extra[arg_name] = _ExtraParameters(**params)
parser.set_defaults(_extra=extra)
return parser
def _parse_extra_parameters(self, args):
# 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 n, e in extra.items():
args[n] = e.validate(n, args[n])
return args
def _construct_parser(self, actionsmap):
"""
Construct the parser with the actions map
Keyword arguments:
- actionsmap -- Multi-level dictionnary of
categories/actions/arguments list
Returns:
Interface relevant's parser object
"""
top_parser = None
iface = self.interface
# Create parser object
if iface ==self.IFACE_CLI:
# TODO: Add descritpion (from __description__)
top_parser = argparse.ArgumentParser()
top_subparsers = top_parser.add_subparsers()
elif iface ==self.IFACE_API:
top_parser = HTTPParser()
## Extract option strings from parameters
def _option_strings(arg_name, arg_params):
if iface ==self.IFACE_CLI:
if arg_name[0] == '-' and 'full' in arg_params:
return [arg_name, arg_params['full']]
return [arg_name]
elif iface ==self.IFACE_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)]
## Extract a key from parameters
def _key(arg_params, key, default=str()):
if key in arg_params:
return arg_params[key]
return default
## Remove extra parameters
def _clean_params(arg_params):
keys = list(_ExtraParameters.AVAILABLE_PARAMETERS)
keys.append('full')
for k in keys:
if k in arg_params:
del arg_params[k]
return arg_params
# Iterate over actions map namespaces
for n in self.get_actionsmap_namespaces():
# Parse general arguments for the cli only
if iface ==self.IFACE_CLI:
for an, ap in actionsmap[n]['general_arguments'].items():
if 'version' in ap:
ap['version'] = ap['version'].replace('%version%', __version__)
top_parser.add_argument(*_option_strings(an, ap), **_clean_params(ap))
del actionsmap[n]['general_arguments']
# Parse categories
for cn, cp in actionsmap[n].items():
if 'actions' not in cp:
continue
# Add category subparsers for the cli only
if iface ==self.IFACE_CLI:
c_help = _key(cp, 'category_help')
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 ==self.IFACE_CLI:
a_help = _key(ap, 'action_help')
parser = subparsers.add_parser(an, help=a_help)
elif iface ==self.IFACE_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

211
src/moulinette/core/api.py Normal file
View file

@ -0,0 +1,211 @@
# -*- coding: utf-8 -*-
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
class APIAuthPlugin(object):
"""
This Bottle plugin manages the authentication for the API access.
"""
name = 'apiauth'
api = 2
def __init__(self):
# TODO: Add options (e.g. session type, content type, ...)
if not os.path.isdir(session_path):
os.makedirs(session_path)
@property
def app(self):
"""Get Bottle application with session integration"""
if hasattr(self, '_app'):
return self._app
raise Exception(_("The APIAuth Plugin is not installed yet."))
def setup(self, app):
"""
Setup the plugin and install the session into the app
Keyword argument:
app -- The associated application object
"""
app.route('/login', name='login', method='POST', callback=self.login)
app.route('/logout', name='logout', method='GET', callback=self.logout)
session_opts = {
'session.type': 'file',
'session.cookie_expires': True,
'session.data_dir': session_path,
'session.secure': True
}
self._app = SessionMiddleware(app, session_opts)
def apply(self, callback, context):
"""
Check authentication before executing the route callback
Keyword argument:
callback -- The route callback
context -- An instance of Route
"""
# Check the authentication
if self._is_authenticated:
if context.name == 'login':
self.logout()
else:
# TODO: Fix this tweak to retrieve uri from the callback
def wrapper(*args, **kwargs):
if hasattr(context.config, '_uri'):
kwargs['_uri'] = context.config._uri
return callback(*args, **kwargs)
return wrapper
# Process login route
if context.name == 'login':
password = request.POST.get('password', None)
if password is not None and self.login(password):
raise HTTPResponse(status=200)
else:
raise HTTPResponse(_("Wrong password"), 401)
# Deny access to the requested route
raise HTTPResponse(_("Unauthorized"), 401)
def login(self, password):
"""
Attempt to log in with the given password
Keyword argument:
password -- Cleartext password
"""
try: YunoHostLDAP(password=password)
except YunoHostError:
return False
else:
session = self._beaker_session
session['authenticated'] = True
session.save()
return True
return False
def logout(self):
"""
Log out and delete the session
"""
# TODO: Delete the cached session file
session = self._beaker_session
session.delete()
## Private methods
@property
def _beaker_session(self):
"""Get Beaker session"""
return request.environ.get('beaker.session')
@property
def _is_authenticated(self):
"""Check authentication"""
# TODO: Clear the session path on password changing to avoid invalid access
if 'authenticated' in self._beaker_session:
return True
return False
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}
"""
def __init__(self, actionsmap, routes={}):
self.actionsmap = actionsmap
# Initialize app and default routes
# TODO: Return OK to 'OPTIONS' xhr requests (l173)
app = Bottle()
app.route(['/api', '/api/<category:re:[a-z]+>'], method='GET', callback=self.doc, skip=['apiauth'])
# Append routes from the actions map
for (m, u) in actionsmap.parser.routes:
app.route(u, method=m, callback=self._route_wrapper, _uri=u)
# Append additional routes
for (m, u), c in routes.items():
app.route(u, method=m, callback=c)
# Define and install a plugin which sets proper header
def apiheader(callback):
def wrapper(*args, **kwargs):
response.content_type = 'application/json'
response.set_header('Access-Control-Allow-Origin', '*')
return callback(*args, **kwargs)
return wrapper
app.install(apiheader)
# Install authentication plugin
apiauth = APIAuthPlugin()
app.install(apiauth)
self._app = apiauth.app
@property
def app(self):
"""Get Bottle application"""
return self._app
def doc(self, category=None):
"""
Get API documentation for a category (all by default)
Keyword argument:
category -- Name of the category
"""
if category is None:
with open(doc_json_path +'/resources.json') as f:
return f.read()
try:
with open(doc_json_path +'/'+ category +'.json') as f:
return f.read()
except IOError:
return 'unknown'
def _route_wrapper(self, *args, **kwargs):
"""Process the relevant action for the request"""
# Retrieve uri
if '_uri' in kwargs:
uri = kwargs['_uri']
del kwargs['_uri']
else:
uri = request.path
# Bring arguments together
params = kwargs
for a in args:
params[a] = True
for k, v in request.params.items():
params[k] = v
# Process the action
# TODO: Catch errors
return self.actionsmap.process(params, (request.method, uri))

View file

@ -1,47 +1,5 @@
# -*- coding: utf-8 -*-
""" License
Copyright (C) 2013 YunoHost
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
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
"""
"""
YunoHost core classes & functions
"""
__credits__ = """
Copyright (C) 2012 YunoHost
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published
by the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
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
"""
__author__ = 'Kload <kload@kload.fr>'
__version__ = '695'
import os
import sys
try:

View file

View file

@ -33,10 +33,11 @@ import time
import re
import socket
import urlparse
from yunohost import YunoHostError, YunoHostLDAP, win_msg, random_password, is_true, validate
from yunohost_domain import domain_list, domain_add
from yunohost_user import user_info, user_list
from yunohost_hook import hook_exec, hook_add, hook_remove
from domain import domain_list, domain_add
from user import user_info, user_list
from hook import hook_exec, hook_add, hook_remove
from ..core.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

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

View file

@ -32,8 +32,9 @@ import json
import yaml
import requests
from urllib import urlopen
from yunohost import YunoHostError, YunoHostLDAP, win_msg, colorize, validate, get_required_args
from yunohost_dyndns import dyndns_subscribe
from dyndns import dyndns_subscribe
from ..core.helpers import YunoHostError, YunoHostLDAP, win_msg, colorize, validate, get_required_args
def domain_list(filter=None, limit=None, offset=None):

View file

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

View file

@ -36,7 +36,8 @@ except ImportError:
sys.stderr.write('Error: Yunohost CLI Require yaml lib\n')
sys.stderr.write('apt-get install python-yaml\n')
sys.exit(1)
from yunohost import YunoHostError, win_msg
from ..core.helpers import YunoHostError, win_msg
def firewall_allow(protocol=None, port=None, ipv6=None, upnp=False):

View file

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

View file

@ -34,10 +34,11 @@ import os.path
import cPickle as pickle
from urllib import urlopen
from datetime import datetime, timedelta
from yunohost import YunoHostError, win_msg
from yunohost_service import (service_enable, service_disable,
from service import (service_enable, service_disable,
service_start, service_stop, service_status)
from ..core.helpers import YunoHostError, win_msg
glances_uri = 'http://127.0.0.1:61209'
stats_path = '/var/lib/yunohost/stats'
crontab_path = '/etc/cron.d/yunohost-monitor'
@ -359,7 +360,7 @@ def monitor_enable(no_stats=False):
# Install crontab
if not no_stats:
cmd = 'yunohost monitor update-stats'
cmd = 'cd /home/admin/dev/moulinette && ./yunohost monitor update-stats'
# day: every 5 min # week: every 1 h # month: every 4 h #
rules = ('*/5 * * * * root %(cmd)s day --no-ldap >> /dev/null\n' + \
'3 * * * * root %(cmd)s week --no-ldap >> /dev/null\n' + \

View file

@ -27,7 +27,8 @@ import yaml
import glob
import subprocess
import os.path
from yunohost import YunoHostError, win_msg
from ..core.helpers import YunoHostError, win_msg
def service_start(names):
@ -169,7 +170,7 @@ def service_log(name, number=50):
if name not in services.keys():
raise YunoHostError(1, _("Unknown service '%s'") % service)
if 'log' in services[name]:
log_list = services[name]['log']
result = {}
@ -253,7 +254,7 @@ def _tail(file, n, offset=None):
pos = f.tell()
lines = f.read().splitlines()
if len(lines) >= to_read or pos == 0:
return lines[-to_read:offset and -offset or None]
return lines[-to_read:offset and -offset or None]
avg_line_length *= 1.3
except IOError: return []

View file

@ -31,11 +31,12 @@ import getpass
import subprocess
import requests
import json
from yunohost import YunoHostError, YunoHostLDAP, validate, colorize, get_required_args, win_msg
from yunohost_domain import domain_add, domain_list
from yunohost_dyndns import dyndns_subscribe
from yunohost_backup import backup_init
from yunohost_app import app_ssowatconf
from domain import domain_add, domain_list
from dyndns import dyndns_subscribe
from backup import backup_init
from app import app_ssowatconf
from ..core.helpers import YunoHostError, YunoHostLDAP, validate, colorize, get_required_args, win_msg
def tools_ldapinit(password=None):

View file

@ -30,9 +30,10 @@ import crypt
import random
import string
import getpass
from yunohost import YunoHostError, YunoHostLDAP, win_msg, colorize, validate, get_required_args
from yunohost_domain import domain_list
from yunohost_hook import hook_callback
from domain import domain_list
from hook import hook_callback
from ..core.helpers import YunoHostError, YunoHostLDAP, win_msg, colorize, validate, get_required_args
def user_list(fields=None, filter=None, limit=None, offset=None):
"""
@ -117,7 +118,7 @@ def user_create(username, firstname, lastname, mail, password):
uid = str(random.randint(200, 99999))
uid_check = os.system("getent passwd " + uid)
gid_check = os.system("getent group " + uid)
# Adapt values for LDAP
fullname = firstname + ' ' + lastname
rdn = 'uid=' + username + ',ou=users'
@ -139,7 +140,7 @@ def user_create(username, firstname, lastname, mail, password):
'uidNumber' : uid,
'homeDirectory' : '/home/' + username,
'loginShell' : '/bin/false'
}
if yldap.add(rdn, attr_dict):

View file

@ -1,3 +0,0 @@
_trial_temp
txrestapi.egg-info
txrestapi/_trial_temp

View file

@ -1,146 +0,0 @@
============
Introduction
============
``txrestapi`` makes it easier to create Twisted REST API services. Normally, one
would create ``Resource`` subclasses defining each segment of a path; this is
cubersome to implement and results in output that isn't very readable.
``txrestapi`` provides an ``APIResource`` class allowing complex mapping of path to
callback (a la Django) with a readable decorator.
===============================
Basic URL callback registration
===============================
First, let's create a bare API service::
>>> from txrestapi.resource import APIResource
>>> api = APIResource()
and a web server to serve it::
>>> from twisted.web.server import Site
>>> from twisted.internet import reactor
>>> site = Site(api, timeout=None)
and a function to make it easy for us to make requests (only for doctest
purposes; normally you would of course use ``reactor.listenTCP(8080, site)``)::
>>> from twisted.web.server import Request
>>> class FakeChannel(object):
... transport = None
>>> def makeRequest(method, path):
... req = Request(FakeChannel(), None)
... req.prepath = req.postpath = None
... req.method = method; req.path = path
... resource = site.getChildWithDefault(path, req)
... return resource.render(req)
We can now register callbacks for paths we care about. We can provide different
callbacks for different methods; they must accept ``request`` as the first
argument::
>>> def get_callback(request): return 'GET callback'
>>> api.register('GET', '^/path/to/method', get_callback)
>>> def post_callback(request): return 'POST callback'
>>> api.register('POST', '^/path/to/method', post_callback)
Then, when we make a call, the request is routed to the proper callback::
>>> print makeRequest('GET', '/path/to/method')
GET callback
>>> print makeRequest('POST', '/path/to/method')
POST callback
We can register multiple callbacks for different requests; the first one that
matches wins::
>>> def default_callback(request):
... return 'Default callback'
>>> api.register('GET', '^/.*$', default_callback) # Matches everything
>>> print makeRequest('GET', '/path/to/method')
GET callback
>>> print makeRequest('GET', '/path/to/different/method')
Default callback
Our default callback, however, will only match GET requests. For a true default
callback, we can either register callbacks for each method individually, or we
can use ALL::
>>> api.register('ALL', '^/.*$', default_callback)
>>> print makeRequest('PUT', '/path/to/method')
Default callback
>>> print makeRequest('DELETE', '/path/to/method')
Default callback
>>> print makeRequest('GET', '/path/to/method')
GET callback
Let's unregister all references to the default callback so it doesn't interfere
with later tests (default callbacks should, of course, always be registered
last, so they don't get called before other callbacks)::
>>> api.unregister(callback=default_callback)
=============
URL Arguments
=============
Since callbacks accept ``request``, they have access to POST data or query
arguments, but we can also pull arguments out of the URL by using named groups
in the regular expression (similar to Django). These will be passed into the
callback as keyword arguments::
>>> def get_info(request, id):
... return 'Information for id %s' % id
>>> api.register('GET', '/(?P<id>[^/]+)/info$', get_info)
>>> print makeRequest('GET', '/someid/info')
Information for id someid
Bear in mind all arguments will come in as strings, so code should be
accordingly defensive.
================
Decorator syntax
================
Registration via the ``register()`` method is somewhat awkward, so decorators
are provided making it much more straightforward. ::
>>> from txrestapi.methods import GET, POST, PUT, ALL
>>> class MyResource(APIResource):
...
... @GET('^/(?P<id>[^/]+)/info')
... def get_info(self, request, id):
... return 'Info for id %s' % id
...
... @PUT('^/(?P<id>[^/]+)/update')
... @POST('^/(?P<id>[^/]+)/update')
... def set_info(self, request, id):
... return "Setting info for id %s" % id
...
... @ALL('^/')
... def default_view(self, request):
... return "I match any URL"
Again, registrations occur top to bottom, so methods should be written from
most specific to least. Also notice that one can use the decorator syntax as
one would expect to register a method as the target for two URLs ::
>>> site = Site(MyResource(), timeout=None)
>>> print makeRequest('GET', '/anid/info')
Info for id anid
>>> print makeRequest('PUT', '/anid/update')
Setting info for id anid
>>> print makeRequest('POST', '/anid/update')
Setting info for id anid
>>> print makeRequest('DELETE', '/anid/delete')
I match any URL
======================
Callback return values
======================
You can return Resource objects from a callback if you wish, allowing you to
have APIs that send you to other kinds of resources, or even other APIs.
Normally, however, you'll most likely want to return strings, which will be
wrapped in a Resource object for convenience.

View file

@ -1 +0,0 @@
#

View file

@ -1,29 +0,0 @@
from zope.interface.advice import addClassAdvisor
def method_factory_factory(method):
def factory(regex):
_f = {}
def decorator(f):
_f[f.__name__] = f
return f
def advisor(cls):
def wrapped(f):
def __init__(self, *args, **kwargs):
f(self, *args, **kwargs)
for func_name in _f:
orig = _f[func_name]
func = getattr(self, func_name)
if func.im_func==orig:
self.register(method, regex, func)
return __init__
cls.__init__ = wrapped(cls.__init__)
return cls
addClassAdvisor(advisor)
return decorator
return factory
ALL = method_factory_factory('ALL')
GET = method_factory_factory('GET')
POST = method_factory_factory('POST')
PUT = method_factory_factory('PUT')
DELETE = method_factory_factory('DELETE')

View file

@ -1,65 +0,0 @@
import re
from itertools import ifilter
from functools import wraps
from twisted.web.resource import Resource, NoResource
class _FakeResource(Resource):
_result = ''
isLeaf = True
def __init__(self, result):
Resource.__init__(self)
self._result = result
def render(self, request):
return self._result
def maybeResource(f):
@wraps(f)
def inner(*args, **kwargs):
result = f(*args, **kwargs)
if not isinstance(result, Resource):
result = _FakeResource(result)
return result
return inner
class APIResource(Resource):
_registry = None
def __init__(self, *args, **kwargs):
Resource.__init__(self, *args, **kwargs)
self._registry = []
def _get_callback(self, request):
filterf = lambda t:t[0] in (request.method, 'ALL')
path_to_check = getattr(request, '_remaining_path', request.path)
for m, r, cb in ifilter(filterf, self._registry):
result = r.search(path_to_check)
if result:
request._remaining_path = path_to_check[result.span()[1]:]
return cb, result.groupdict()
return None, None
def register(self, method, regex, callback):
self._registry.append((method, re.compile(regex), callback))
def unregister(self, method=None, regex=None, callback=None):
if regex is not None: regex = re.compile(regex)
for m, r, cb in self._registry[:]:
if not method or (method and m==method):
if not regex or (regex and r==regex):
if not callback or (callback and cb==callback):
self._registry.remove((m, r, cb))
def getChild(self, name, request):
r = self.children.get(name, None)
if r is None:
# Go into the thing
callback, args = self._get_callback(request)
if callback is None:
return NoResource()
else:
return maybeResource(callback)(request, **args)
else:
return r

View file

@ -1,7 +0,0 @@
from twisted.web.server import Site
from .resource import APIResource
class RESTfulService(Site):
def __init__(self, port=8080):
self.root = APIResource()

View file

@ -1,194 +0,0 @@
import txrestapi
__package__="txrestapi"
import re
import os.path
from twisted.internet import reactor
from twisted.internet.defer import inlineCallbacks
from twisted.web.resource import Resource, NoResource
from twisted.web.server import Request, Site
from twisted.web.client import getPage
from twisted.trial import unittest
from .resource import APIResource
from .methods import GET, PUT
class FakeChannel(object):
transport = None
def getRequest(method, url):
req = Request(FakeChannel(), None)
req.method = method
req.path = url
return req
class APIResourceTest(unittest.TestCase):
def test_returns_normal_resources(self):
r = APIResource()
a = Resource()
r.putChild('a', a)
req = Request(FakeChannel(), None)
a_ = r.getChild('a', req)
self.assertEqual(a, a_)
def test_registry(self):
compiled = re.compile('regex')
r = APIResource()
r.register('GET', 'regex', None)
self.assertEqual([x[0] for x in r._registry], ['GET'])
self.assertEqual(r._registry[0], ('GET', compiled, None))
def test_method_matching(self):
r = APIResource()
r.register('GET', 'regex', 1)
r.register('PUT', 'regex', 2)
r.register('GET', 'another', 3)
req = getRequest('GET', 'regex')
result = r._get_callback(req)
self.assert_(result)
self.assertEqual(result[0], 1)
req = getRequest('PUT', 'regex')
result = r._get_callback(req)
self.assert_(result)
self.assertEqual(result[0], 2)
req = getRequest('GET', 'another')
result = r._get_callback(req)
self.assert_(result)
self.assertEqual(result[0], 3)
req = getRequest('PUT', 'another')
result = r._get_callback(req)
self.assertEqual(result, (None, None))
def test_callback(self):
marker = object()
def cb(request):
return marker
r = APIResource()
r.register('GET', 'regex', cb)
req = getRequest('GET', 'regex')
result = r.getChild('regex', req)
self.assertEqual(result.render(req), marker)
def test_longerpath(self):
marker = object()
r = APIResource()
def cb(request):
return marker
r.register('GET', '/regex/a/b/c', cb)
req = getRequest('GET', '/regex/a/b/c')
result = r.getChild('regex', req)
self.assertEqual(result.render(req), marker)
def test_args(self):
r = APIResource()
def cb(request, **kwargs):
return kwargs
r.register('GET', '/(?P<a>[^/]*)/a/(?P<b>[^/]*)/c', cb)
req = getRequest('GET', '/regex/a/b/c')
result = r.getChild('regex', req)
self.assertEqual(sorted(result.render(req).keys()), ['a', 'b'])
def test_order(self):
r = APIResource()
def cb1(request, **kwargs):
kwargs.update({'cb1':True})
return kwargs
def cb(request, **kwargs):
return kwargs
# Register two regexes that will match
r.register('GET', '/(?P<a>[^/]*)/a/(?P<b>[^/]*)/c', cb1)
r.register('GET', '/(?P<a>[^/]*)/a/(?P<b>[^/]*)', cb)
req = getRequest('GET', '/regex/a/b/c')
result = r.getChild('regex', req)
# Make sure the first one got it
self.assert_('cb1' in result.render(req))
def test_no_resource(self):
r = APIResource()
r.register('GET', '^/(?P<a>[^/]*)/a/(?P<b>[^/]*)$', None)
req = getRequest('GET', '/definitely/not/a/match')
result = r.getChild('regex', req)
self.assert_(isinstance(result, NoResource))
def test_all(self):
r = APIResource()
def get_cb(r): return 'GET'
def put_cb(r): return 'PUT'
def all_cb(r): return 'ALL'
r.register('GET', '^path', get_cb)
r.register('ALL', '^path', all_cb)
r.register('PUT', '^path', put_cb)
# Test that the ALL registration picks it up before the PUT one
for method in ('GET', 'PUT', 'ALL'):
req = getRequest(method, 'path')
result = r.getChild('path', req)
self.assertEqual(result.render(req), 'ALL' if method=='PUT' else method)
class TestResource(Resource):
isLeaf = True
def render(self, request):
return 'aresource'
class TestAPI(APIResource):
@GET('^/(?P<a>test[^/]*)/?')
def _on_test_get(self, request, a):
return 'GET %s' % a
@PUT('^/(?P<a>test[^/]*)/?')
def _on_test_put(self, request, a):
return 'PUT %s' % a
@GET('^/gettest')
def _on_gettest(self, request):
return TestResource()
class DecoratorsTest(unittest.TestCase):
def _listen(self, site):
return reactor.listenTCP(0, site, interface="127.0.0.1")
def setUp(self):
r = TestAPI()
site = Site(r, timeout=None)
self.port = self._listen(site)
self.portno = self.port.getHost().port
def tearDown(self):
return self.port.stopListening()
def getURL(self, path):
return "http://127.0.0.1:%d/%s" % (self.portno, path)
@inlineCallbacks
def test_get(self):
url = self.getURL('test_thing/')
result = yield getPage(url, method='GET')
self.assertEqual(result, 'GET test_thing')
@inlineCallbacks
def test_put(self):
url = self.getURL('test_thing/')
result = yield getPage(url, method='PUT')
self.assertEqual(result, 'PUT test_thing')
@inlineCallbacks
def test_resource_wrapper(self):
url = self.getURL('gettest')
result = yield getPage(url, method='GET')
self.assertEqual(result, 'aresource')
def test_suite():
import unittest as ut
suite = unittest.TestSuite()
suite.addTest(ut.makeSuite(DecoratorsTest))
suite.addTest(ut.makeSuite(APIResourceTest))
suite.addTest(unittest.doctest.DocFileSuite(os.path.join('..', 'README.rst')))
return suite