mirror of
https://github.com/YunoHost/moulinette.git
synced 2024-09-03 20:06:31 +02:00
Initial refactoring
This commit is contained in:
parent
aa1c87c4d0
commit
a802bcfd6f
31 changed files with 941 additions and 527 deletions
42
bin/yunohost
Executable file
42
bin/yunohost
Executable 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
43
bin/yunohost-api
Executable 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)
|
|
@ -95,7 +95,7 @@ user:
|
||||||
### user_delete()
|
### user_delete()
|
||||||
delete:
|
delete:
|
||||||
action_help: Delete user
|
action_help: Delete user
|
||||||
api: 'DELETE /users/{users}'
|
api: 'DELETE /users/<users>'
|
||||||
arguments:
|
arguments:
|
||||||
-u:
|
-u:
|
||||||
full: --users
|
full: --users
|
||||||
|
@ -109,7 +109,7 @@ user:
|
||||||
### user_update()
|
### user_update()
|
||||||
update:
|
update:
|
||||||
action_help: Update user informations
|
action_help: Update user informations
|
||||||
api: 'PUT /users/{username}'
|
api: 'PUT /users/<username>'
|
||||||
arguments:
|
arguments:
|
||||||
username:
|
username:
|
||||||
help: Username of user to update
|
help: Username of user to update
|
||||||
|
@ -143,7 +143,7 @@ user:
|
||||||
### user_info()
|
### user_info()
|
||||||
info:
|
info:
|
||||||
action_help: Get user informations
|
action_help: Get user informations
|
||||||
api: 'GET /users/{username}'
|
api: 'GET /users/<username>'
|
||||||
arguments:
|
arguments:
|
||||||
username:
|
username:
|
||||||
help: Username or mail to get informations
|
help: Username or mail to get informations
|
||||||
|
@ -202,7 +202,7 @@ domain:
|
||||||
### domain_info()
|
### domain_info()
|
||||||
info:
|
info:
|
||||||
action_help: Get domain informations
|
action_help: Get domain informations
|
||||||
api: 'GET /domains/{domain}'
|
api: 'GET /domains/<domain>'
|
||||||
arguments:
|
arguments:
|
||||||
domain:
|
domain:
|
||||||
help: ""
|
help: ""
|
||||||
|
@ -266,7 +266,7 @@ app:
|
||||||
### app_info()
|
### app_info()
|
||||||
info:
|
info:
|
||||||
action_help: Get app info
|
action_help: Get app info
|
||||||
api: GET /app/{app}
|
api: GET /app/<app>
|
||||||
arguments:
|
arguments:
|
||||||
app:
|
app:
|
||||||
help: Specific app ID
|
help: Specific app ID
|
||||||
|
@ -310,7 +310,7 @@ app:
|
||||||
### app_remove() TODO: Write help
|
### app_remove() TODO: Write help
|
||||||
remove:
|
remove:
|
||||||
action_help: Remove app
|
action_help: Remove app
|
||||||
api: DELETE /app/{app}
|
api: DELETE /app/<app>
|
||||||
arguments:
|
arguments:
|
||||||
app:
|
app:
|
||||||
help: App(s) to delete
|
help: App(s) to delete
|
||||||
|
@ -333,7 +333,7 @@ app:
|
||||||
### app_setting()
|
### app_setting()
|
||||||
setting:
|
setting:
|
||||||
action_help: Set ou get an app setting value
|
action_help: Set ou get an app setting value
|
||||||
api: GET /app/{app}/setting
|
api: GET /app/<app>/setting
|
||||||
arguments:
|
arguments:
|
||||||
app:
|
app:
|
||||||
help: App ID
|
help: App ID
|
||||||
|
@ -350,7 +350,7 @@ app:
|
||||||
### app_service()
|
### app_service()
|
||||||
service:
|
service:
|
||||||
action_help: Add or remove a YunoHost monitored service
|
action_help: Add or remove a YunoHost monitored service
|
||||||
api: POST /app/service/{service}
|
api: POST /app/service/<service>
|
||||||
arguments:
|
arguments:
|
||||||
service:
|
service:
|
||||||
help: Service to add/remove
|
help: Service to add/remove
|
|
@ -1,6 +1,6 @@
|
||||||
UPNP:
|
UPNP:
|
||||||
cron: false
|
cron: false
|
||||||
ports:
|
ports:
|
||||||
TCP: [22, 25, 53, 80, 443, 465, 993, 5222, 5269, 5280, 6767, 7676]
|
TCP: [22, 25, 53, 80, 443, 465, 993, 5222, 5269, 5280, 6767, 7676]
|
||||||
UDP: [53, 137, 138]
|
UDP: [53, 137, 138]
|
||||||
ipv4:
|
ipv4:
|
|
@ -7,14 +7,14 @@ COMPREPLY=()
|
||||||
|
|
||||||
argc=${COMP_CWORD}
|
argc=${COMP_CWORD}
|
||||||
cur="${COMP_WORDS[argc]}"
|
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 ',' ' ')
|
opts=$(yunohost -h | sed -n "/usage/,/}/p" | awk -F"{" '{print $2}' | awk -F"}" '{print $1}' | tr ',' ' ')
|
||||||
|
|
||||||
if [[ $argc = 1 ]];
|
if [[ $argc = 1 ]];
|
||||||
then
|
then
|
||||||
COMPREPLY=( $(compgen -W "$opts --help" -- $cur ) )
|
COMPREPLY=( $(compgen -W "$opts --help" -- $cur ) )
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$prev" != "--help" ]];
|
if [[ "$prev" != "--help" ]];
|
||||||
then
|
then
|
||||||
if [[ $argc = 2 ]];
|
if [[ $argc = 2 ]];
|
||||||
|
@ -23,9 +23,9 @@ then
|
||||||
COMPREPLY=( $(compgen -W "$opts2 --help" -- $cur ) )
|
COMPREPLY=( $(compgen -W "$opts2 --help" -- $cur ) )
|
||||||
elif [[ $argc = 3 ]];
|
elif [[ $argc = 3 ]];
|
||||||
then
|
then
|
||||||
COMPREPLY=( $(compgen -W "--help" $cur ) )
|
COMPREPLY=( $(compgen -W "--help" $cur ) )
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
COMPREPLY=()
|
COMPREPLY=()
|
||||||
fi
|
fi
|
||||||
|
|
89
src/moulinette/__init__.py
Executable file
89
src/moulinette/__init__.py
Executable 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
16
src/moulinette/config.py
Normal 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'
|
0
src/moulinette/core/__init__.py
Executable file
0
src/moulinette/core/__init__.py
Executable file
490
src/moulinette/core/actionsmap.py
Normal file
490
src/moulinette/core/actionsmap.py
Normal 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
211
src/moulinette/core/api.py
Normal 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))
|
|
@ -1,47 +1,5 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- 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 os
|
||||||
import sys
|
import sys
|
||||||
try:
|
try:
|
0
src/moulinette/yunohost/__init__.py
Executable file
0
src/moulinette/yunohost/__init__.py
Executable file
|
@ -33,10 +33,11 @@ import time
|
||||||
import re
|
import re
|
||||||
import socket
|
import socket
|
||||||
import urlparse
|
import urlparse
|
||||||
from yunohost import YunoHostError, YunoHostLDAP, win_msg, random_password, is_true, validate
|
from domain import domain_list, domain_add
|
||||||
from yunohost_domain import domain_list, domain_add
|
from user import user_info, user_list
|
||||||
from yunohost_user import user_info, user_list
|
from hook import hook_exec, hook_add, hook_remove
|
||||||
from yunohost_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'
|
repo_path = '/var/cache/yunohost/repo'
|
||||||
apps_path = '/usr/share/yunohost/apps'
|
apps_path = '/usr/share/yunohost/apps'
|
|
@ -28,7 +28,8 @@ import sys
|
||||||
import json
|
import json
|
||||||
import yaml
|
import yaml
|
||||||
import glob
|
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):
|
def backup_init(helper=False):
|
||||||
"""
|
"""
|
|
@ -32,8 +32,9 @@ import json
|
||||||
import yaml
|
import yaml
|
||||||
import requests
|
import requests
|
||||||
from urllib import urlopen
|
from urllib import urlopen
|
||||||
from yunohost import YunoHostError, YunoHostLDAP, win_msg, colorize, validate, get_required_args
|
from dyndns import dyndns_subscribe
|
||||||
from yunohost_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):
|
def domain_list(filter=None, limit=None, offset=None):
|
|
@ -29,7 +29,8 @@ import requests
|
||||||
import json
|
import json
|
||||||
import glob
|
import glob
|
||||||
import base64
|
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):
|
def dyndns_subscribe(subscribe_host="dyndns.yunohost.org", domain=None, key=None):
|
||||||
"""
|
"""
|
|
@ -36,7 +36,8 @@ except ImportError:
|
||||||
sys.stderr.write('Error: Yunohost CLI Require yaml lib\n')
|
sys.stderr.write('Error: Yunohost CLI Require yaml lib\n')
|
||||||
sys.stderr.write('apt-get install python-yaml\n')
|
sys.stderr.write('apt-get install python-yaml\n')
|
||||||
sys.exit(1)
|
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):
|
def firewall_allow(protocol=None, port=None, ipv6=None, upnp=False):
|
|
@ -27,7 +27,8 @@ import os
|
||||||
import sys
|
import sys
|
||||||
import re
|
import re
|
||||||
import json
|
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/'
|
hook_folder = '/usr/share/yunohost/hooks/'
|
||||||
|
|
|
@ -34,10 +34,11 @@ import os.path
|
||||||
import cPickle as pickle
|
import cPickle as pickle
|
||||||
from urllib import urlopen
|
from urllib import urlopen
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from yunohost import YunoHostError, win_msg
|
from service import (service_enable, service_disable,
|
||||||
from yunohost_service import (service_enable, service_disable,
|
|
||||||
service_start, service_stop, service_status)
|
service_start, service_stop, service_status)
|
||||||
|
|
||||||
|
from ..core.helpers import YunoHostError, win_msg
|
||||||
|
|
||||||
glances_uri = 'http://127.0.0.1:61209'
|
glances_uri = 'http://127.0.0.1:61209'
|
||||||
stats_path = '/var/lib/yunohost/stats'
|
stats_path = '/var/lib/yunohost/stats'
|
||||||
crontab_path = '/etc/cron.d/yunohost-monitor'
|
crontab_path = '/etc/cron.d/yunohost-monitor'
|
||||||
|
@ -359,7 +360,7 @@ def monitor_enable(no_stats=False):
|
||||||
|
|
||||||
# Install crontab
|
# Install crontab
|
||||||
if not no_stats:
|
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 #
|
# day: every 5 min # week: every 1 h # month: every 4 h #
|
||||||
rules = ('*/5 * * * * root %(cmd)s day --no-ldap >> /dev/null\n' + \
|
rules = ('*/5 * * * * root %(cmd)s day --no-ldap >> /dev/null\n' + \
|
||||||
'3 * * * * root %(cmd)s week --no-ldap >> /dev/null\n' + \
|
'3 * * * * root %(cmd)s week --no-ldap >> /dev/null\n' + \
|
|
@ -27,7 +27,8 @@ import yaml
|
||||||
import glob
|
import glob
|
||||||
import subprocess
|
import subprocess
|
||||||
import os.path
|
import os.path
|
||||||
from yunohost import YunoHostError, win_msg
|
|
||||||
|
from ..core.helpers import YunoHostError, win_msg
|
||||||
|
|
||||||
|
|
||||||
def service_start(names):
|
def service_start(names):
|
||||||
|
@ -169,7 +170,7 @@ def service_log(name, number=50):
|
||||||
|
|
||||||
if name not in services.keys():
|
if name not in services.keys():
|
||||||
raise YunoHostError(1, _("Unknown service '%s'") % service)
|
raise YunoHostError(1, _("Unknown service '%s'") % service)
|
||||||
|
|
||||||
if 'log' in services[name]:
|
if 'log' in services[name]:
|
||||||
log_list = services[name]['log']
|
log_list = services[name]['log']
|
||||||
result = {}
|
result = {}
|
||||||
|
@ -253,7 +254,7 @@ def _tail(file, n, offset=None):
|
||||||
pos = f.tell()
|
pos = f.tell()
|
||||||
lines = f.read().splitlines()
|
lines = f.read().splitlines()
|
||||||
if len(lines) >= to_read or pos == 0:
|
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
|
avg_line_length *= 1.3
|
||||||
|
|
||||||
except IOError: return []
|
except IOError: return []
|
|
@ -31,11 +31,12 @@ import getpass
|
||||||
import subprocess
|
import subprocess
|
||||||
import requests
|
import requests
|
||||||
import json
|
import json
|
||||||
from yunohost import YunoHostError, YunoHostLDAP, validate, colorize, get_required_args, win_msg
|
from domain import domain_add, domain_list
|
||||||
from yunohost_domain import domain_add, domain_list
|
from dyndns import dyndns_subscribe
|
||||||
from yunohost_dyndns import dyndns_subscribe
|
from backup import backup_init
|
||||||
from yunohost_backup import backup_init
|
from app import app_ssowatconf
|
||||||
from yunohost_app import app_ssowatconf
|
|
||||||
|
from ..core.helpers import YunoHostError, YunoHostLDAP, validate, colorize, get_required_args, win_msg
|
||||||
|
|
||||||
|
|
||||||
def tools_ldapinit(password=None):
|
def tools_ldapinit(password=None):
|
|
@ -30,9 +30,10 @@ import crypt
|
||||||
import random
|
import random
|
||||||
import string
|
import string
|
||||||
import getpass
|
import getpass
|
||||||
from yunohost import YunoHostError, YunoHostLDAP, win_msg, colorize, validate, get_required_args
|
from domain import domain_list
|
||||||
from yunohost_domain import domain_list
|
from hook import hook_callback
|
||||||
from yunohost_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):
|
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 = str(random.randint(200, 99999))
|
||||||
uid_check = os.system("getent passwd " + uid)
|
uid_check = os.system("getent passwd " + uid)
|
||||||
gid_check = os.system("getent group " + uid)
|
gid_check = os.system("getent group " + uid)
|
||||||
|
|
||||||
# Adapt values for LDAP
|
# Adapt values for LDAP
|
||||||
fullname = firstname + ' ' + lastname
|
fullname = firstname + ' ' + lastname
|
||||||
rdn = 'uid=' + username + ',ou=users'
|
rdn = 'uid=' + username + ',ou=users'
|
||||||
|
@ -139,7 +140,7 @@ def user_create(username, firstname, lastname, mail, password):
|
||||||
'uidNumber' : uid,
|
'uidNumber' : uid,
|
||||||
'homeDirectory' : '/home/' + username,
|
'homeDirectory' : '/home/' + username,
|
||||||
'loginShell' : '/bin/false'
|
'loginShell' : '/bin/false'
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if yldap.add(rdn, attr_dict):
|
if yldap.add(rdn, attr_dict):
|
3
txrestapi/.gitignore
vendored
3
txrestapi/.gitignore
vendored
|
@ -1,3 +0,0 @@
|
||||||
_trial_temp
|
|
||||||
txrestapi.egg-info
|
|
||||||
txrestapi/_trial_temp
|
|
|
@ -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.
|
|
|
@ -1 +0,0 @@
|
||||||
#
|
|
|
@ -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')
|
|
|
@ -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
|
|
|
@ -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()
|
|
|
@ -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
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue