Merge branch 'dev', remote-tracking branch 'origin/dev' into dev

* origin/dev:
  REST code and header fixes
  Add txrestapi
  REST API for moulinette :d
  Init backup functions
  sudo_ldap_scheme.yml

* dev:
This commit is contained in:
titoko 2013-06-29 13:55:28 +02:00
commit 89f8cb2a58
17 changed files with 851 additions and 158 deletions

View file

@ -51,6 +51,7 @@ user:
### user_list()
list:
action_help: List users
api: GET /user/list
arguments:
--fields:
help: fields to fetch
@ -68,6 +69,7 @@ user:
### user_create()
create:
action_help: Create user
api: POST /user
arguments:
-u:
full: --username
@ -93,6 +95,7 @@ user:
### user_delete()
delete:
action_help: Delete user
api: DELETE /user
arguments:
-u:
full: --users
@ -106,6 +109,7 @@ user:
### user_update()
update:
action_help: Update user informations
api: PUT /user
arguments:
username:
help: Username of user to update
@ -139,6 +143,7 @@ user:
### user_info()
info:
action_help: Get user informations
api: GET /user
arguments:
user-or-mail:
help: Username or mail to get informations
@ -154,6 +159,7 @@ domain:
### domain_list()
list:
action_help: List domains
api: GET /domain/list
arguments:
-f:
full: --filter
@ -168,6 +174,7 @@ domain:
### domain_add()
add:
action_help: Create a custom domain
api: POST /domain
arguments:
domains:
help: Domain name to add
@ -181,6 +188,7 @@ domain:
### domain_remove()
remove:
action_help: Delete domains
api: DELETE /domain
arguments:
domains:
help: Domain(s) to delete
@ -190,18 +198,12 @@ domain:
### domain_info()
info:
action_help: Get domain informations
api: GET /domain
arguments:
domain:
help: ""
pattern: '^([a-zA-Z0-9]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)(\.[a-zA-Z0-9]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)*(\.[a-zA-Z]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)$'
### domain_renewcert()
renewcert:
action_help: Renew domain certificate
arguments:
domain:
help: ""
pattern: '^([a-zA-Z0-9]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)(\.[a-zA-Z0-9]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)*(\.[a-zA-Z]{1}([a-zA-Z0-9\-]*[a-zA-Z0-9])*)$'
#############################
# App #
@ -213,6 +215,7 @@ app:
### app_fetchlist()
fetchlist:
action_help: Fetch application list from app server
api: PUT /app/lists
arguments:
-u:
full: --url
@ -224,10 +227,12 @@ app:
### app_listlists()
listlists:
action_help: List fetched lists
api: GET /app/lists
### app_removelist()
removelist:
action_help: Remove list from the repositories
api: DELETE /app/lists
arguments:
-n:
full: --name
@ -238,6 +243,7 @@ app:
### app_list()
list:
action_help: List apps
api: GET /app/list
arguments:
-l:
full: --limit
@ -256,6 +262,7 @@ app:
### app_map()
map:
action_help: List apps by domain
api: GET /app/map
arguments:
-a:
full: --app
@ -268,6 +275,7 @@ app:
### app_install() TODO: Write help
install:
action_help: Install apps
api: POST /app
arguments:
app:
help: App to install
@ -289,6 +297,7 @@ app:
### app_remove() TODO: Write help
remove:
action_help: Remove app
api: DELETE /app
arguments:
app:
help: App(s) to delete
@ -300,6 +309,7 @@ app:
### app_upgrade()
upgrade:
action_help: Upgrade app
api: PUT /app
arguments:
app:
help: App(s) to upgrade (default all)
@ -318,6 +328,7 @@ app:
### app_info() TODO: Write help
info:
action_help: Get app informations
api: GET /app
arguments:
app:
help: App ID
@ -333,6 +344,7 @@ app:
### app_addaccess() TODO: Write help
addaccess:
action_help: Grant access right to users (everyone by default)
api: PUT /app/access
arguments:
apps:
nargs: "+"
@ -343,6 +355,7 @@ app:
### app_removeaccess() TODO: Write help
removeaccess:
action_help: Revoke access right to users (everyone by default)
api: DELETE /app/access
arguments:
apps:
nargs: "+"
@ -352,46 +365,20 @@ app:
#############################
# Repository #
# Backup #
#############################
repo:
category_help: Manage app repositories
backup:
category_help: Manage backups
actions:
### repo_list()
list:
action_help: List repositories
### backup_init()
init:
action_help: Init Tahoe-LAFS configuration
api: POST /backup/init
arguments:
-f:
full: --filter
help: LDAP filter used to search
-l:
full: --limit
help: Maximum number of repository fetched
-o:
full: --offset
help: Starting number for repository fetching
### repo_add()
add:
action_help: Add app repository
arguments:
url:
help: URL of the repository
-n:
full: --name
help: Unique name of the repository
### repo_remove()
remove:
action_help: Remove repository
arguments:
repo:
help: Name or URL of the repository
### repo_update()
update:
action_help: Update app list from the repositories
--helper:
help: Init as a helper node rather than a "helped" one
action: store_true
#############################
@ -466,10 +453,12 @@ firewall:
### firewall_list()
list:
action_help: List all firewall rules
api: GET /firewall/list
### firewall_reload()
reload:
action_help: Reload all firewall rules
action_help: Reload all firewall rules
api: PUT /firewall/list
arguments:
-u:
full: --upnp
@ -478,6 +467,7 @@ firewall:
### firewall_allow()
allow:
action_help: Allow connection port/protocol
api: POST /firewall/port
arguments:
port:
help: Port to open
@ -500,6 +490,7 @@ firewall:
### firewall_disallow()
disallow:
action_help: Disallow connection
api: DELETE /firewall/port
arguments:
port:
help: Port to open
@ -522,21 +513,25 @@ firewall:
### firewall_installupnp()
installupnp:
action_help: Add upnp cron
api: POST /firewall/upnp
### firewall_removeupnp()
removeupnp:
action_help: Remove upnp cron
api: DELETE /firewall/upnp
### firewall_stop()
stop:
action_help: Stop iptables and ip6tables
api: DELETE /firewall
### firewall_checkupnp()
checkupnp:
action_help: check if UPNP is install or not (0 yes 1 no)
api: GET /firewall/upnp
#############################
@ -549,6 +544,7 @@ dyndns:
### dyndns_subscribe()
subscribe:
action_help: Subscribe to a DynDNS service
api: POST /dyndns
arguments:
--subscribe-host:
help: Dynette HTTP API to subscribe to
@ -563,6 +559,7 @@ dyndns:
### dyndns_update()
update:
action_help: Update IP on DynDNS platform
api: PUT /dyndns
arguments:
--dyn-host:
help: Dynette DNS server to inform
@ -580,10 +577,12 @@ dyndns:
### dyndns_installcron()
installcron:
action_help: Install IP update cron
api: POST /dyndns/cron
### dyndns_removecron()
removecron:
action_help: Remove IP update cron
api: DELETE /dyndns/cron
#############################
@ -596,10 +595,12 @@ tools:
### tools_ldapinit()
ldapinit:
action_help: YunoHost LDAP initialization
api: POST /ldap
### tools_adminpw()
adminpw:
action_help: Change admin password
api: PUT /adminpw
arguments:
-o:
full: --old-password
@ -613,6 +614,7 @@ tools:
### tools_maindomain()
maindomain:
action_help: Main domain change tool
api: PUT /domain/main
arguments:
-o:
full: --old-domain
@ -625,6 +627,7 @@ tools:
### tools_postinstall()
postinstall:
action_help: YunoHost post-install
api: POST /postinstall
arguments:
-d:
full: --domain

View file

@ -23,7 +23,7 @@ parents:
- organizationalUnit
- top
childs:
children:
cn=admins,ou=groups:
cn: admins
gidNumber: "4001"

26
sudo_ldap_scheme.yml Normal file
View file

@ -0,0 +1,26 @@
parents:
ou=sudo:
ou: sudo
objectClass:
- organizationalUnit
- top
children:
cn=admin,ou=sudo:
cn: admin
sudoUser: admin
sudoHost: ALL
sudoCommand: ALL
sudoOption: "!authenticate"
objectClass:
- sudoRole
- top
cn=yunohost-admin,ou=sudo:
cn: yunohost-admin
sudoUser: yunohost-admin
sudoHost: ALL
sudoCommand: /usr/bin/yunohost
sudoOption: "!authenticate"
objectClass:
- sudoRole
- top

3
txrestapi/.gitignore vendored Normal file
View file

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

146
txrestapi/README.rst Normal file
View file

@ -0,0 +1,146 @@
============
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
txrestapi/setup.cfg Normal file
View file

@ -0,0 +1 @@
[egg_info]

26
txrestapi/setup.py Normal file
View file

@ -0,0 +1,26 @@
from setuptools import setup, find_packages
import sys, os
version = '0.1'
setup(name='txrestapi',
version=version,
description="Easing the creation of REST API services in Python",
long_description="""\
""",
classifiers=[], # Get strings from http://pypi.python.org/pypi?%3Aaction=list_classifiers
keywords='',
author='Ian McCracken',
author_email='ian.mccracken@gmail.com',
url='http://github.com/iancmcc/txrestapi',
license='MIT',
packages=find_packages(exclude=['ez_setup', 'examples', 'tests']),
include_package_data=True,
zip_safe=False,
install_requires=[
# -*- Extra requirements: -*-
],
entry_points="""
# -*- Entry points: -*-
""",
)

View file

@ -0,0 +1 @@
#

View file

@ -0,0 +1,29 @@
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

@ -0,0 +1,65 @@
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

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

View file

@ -0,0 +1,194 @@
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

111
yunohost
View file

@ -38,121 +38,12 @@ if not __debug__:
gettext.install('YunoHost')
try:
from yunohost import YunoHostError, YunoHostLDAP, str_to_func, colorize, pretty_print_dict, display_error, validate, win
from yunohost import YunoHostError, YunoHostLDAP, str_to_func, colorize, pretty_print_dict, display_error, validate, win, parse_dict
except ImportError:
sys.stderr.write('Error: Yunohost CLI Require YunoHost lib\n')
sys.exit(1)
def parse_dict(action_map):
"""
Turn action dictionnary to parser, subparsers and arguments
Keyword arguments:
action_map -- Multi-level dictionnary of categories/actions/arguments list
Returns:
Namespace of args
"""
# Intialize parsers
parsers = subparsers_category = subparsers_action = {}
parsers['general'] = argparse.ArgumentParser()
subparsers = parsers['general'].add_subparsers()
new_args = []
patterns = {}
# Add general arguments
for arg_name, arg_params in action_map['general_arguments'].items():
if 'full' in arg_params:
arg_names = [arg_name, arg_params['full']]
arg_fullname = arg_params['full']
del arg_params['full']
else: arg_names = [arg_name]
parsers['general'].add_argument(*arg_names, **arg_params)
del action_map['general_arguments']
# Split categories into subparsers
for category, category_params in action_map.items():
if 'category_help' not in category_params: category_params['category_help'] = ''
subparsers_category[category] = subparsers.add_parser(category, help=category_params['category_help'])
subparsers_action[category] = subparsers_category[category].add_subparsers()
# Split actions
if 'actions' in category_params:
for action, action_params in category_params['actions'].items():
if 'action_help' not in action_params: action_params['action_help'] = ''
parsers[category + '_' + action] = subparsers_action[category].add_parser(action, help=action_params['action_help'])
# Set the action s related function
parsers[category + '_' + action].set_defaults(
func=str_to_func('yunohost_' + category + '.'
+ category + '_' + action))
# Add arguments
if 'arguments' in action_params:
for arg_name, arg_params in action_params['arguments'].items():
arg_fullname = False
if 'password' in arg_params:
if arg_params['password']: is_password = True
del arg_params['password']
else: is_password = False
if 'full' in arg_params:
arg_names = [arg_name, arg_params['full']]
arg_fullname = arg_params['full']
del arg_params['full']
else: arg_names = [arg_name]
if 'ask' in arg_params:
require_input = True
if '-h' in sys.argv or '--help' in sys.argv:
require_input = False
if (category != sys.argv[1]) or (action != sys.argv[2]):
require_input = False
for name in arg_names:
if name in sys.argv[2:]: require_input = False
if require_input:
if is_password:
if os.isatty(1):
pwd1 = getpass.getpass(colorize(arg_params['ask'] + ': ', 'cyan'))
pwd2 = getpass.getpass(colorize('Retype ' + arg_params['ask'][0].lower() + arg_params['ask'][1:] + ': ', 'cyan'))
if pwd1 != pwd2:
raise YunoHostError(22, _("Passwords don't match"))
sys.exit(1)
else:
raise YunoHostError(22, _("Missing arguments") + ': ' + arg_name)
if arg_name[0] == '-': arg_extend = [arg_name, pwd1]
else: arg_extend = [pwd1]
else:
if os.isatty(1):
arg_value = raw_input(colorize(arg_params['ask'] + ': ', 'cyan'))
else:
raise YunoHostError(22, _("Missing arguments") + ': ' + arg_name)
if arg_name[0] == '-': arg_extend = [arg_name, arg_value]
else: arg_extend = [arg_value]
new_args.extend(arg_extend)
del arg_params['ask']
if 'pattern' in arg_params:
if (category == sys.argv[1]) and (action == sys.argv[2]):
if 'dest' in arg_params: name = arg_params['dest']
elif arg_fullname: name = arg_fullname[2:]
else: name = arg_name
name = name.replace('-', '_')
patterns[name] = arg_params['pattern']
del arg_params['pattern']
parsers[category + '_' + action].add_argument(*arg_names, **arg_params)
args = parsers['general'].parse_args(sys.argv.extend(new_args))
args_dict = vars(args)
for key, value in patterns.items():
validate(value, args_dict[key])
return args
def main():
"""
Main instructions

View file

@ -9,11 +9,15 @@ except ImportError:
sys.stderr.write('apt-get install python-ldap\n')
sys.exit(1)
import ldap.modlist as modlist
import yaml
import json
import re
import getpass
import random
import string
import argparse
import gettext
import getpass
if not __debug__:
import traceback
@ -93,8 +97,8 @@ def win_msg(astr):
global win
if os.isatty(1):
print('\n' + colorize(_("Success: "), 'green') + astr + '\n')
else:
win.append(astr)
win.append(astr)
@ -444,3 +448,112 @@ class YunoHostLDAP(Singleton):
else:
raise YunoHostError(17, _('Attribute already exists') + ' "' + attr + '=' + value + '"')
return True
def parse_dict(action_map):
"""
Turn action dictionnary to parser, subparsers and arguments
Keyword arguments:
action_map -- Multi-level dictionnary of categories/actions/arguments list
Returns:
Namespace of args
"""
# Intialize parsers
parsers = subparsers_category = subparsers_action = {}
parsers['general'] = argparse.ArgumentParser()
subparsers = parsers['general'].add_subparsers()
new_args = []
patterns = {}
# Add general arguments
for arg_name, arg_params in action_map['general_arguments'].items():
if 'full' in arg_params:
arg_names = [arg_name, arg_params['full']]
arg_fullname = arg_params['full']
del arg_params['full']
else: arg_names = [arg_name]
parsers['general'].add_argument(*arg_names, **arg_params)
del action_map['general_arguments']
# Split categories into subparsers
for category, category_params in action_map.items():
if 'category_help' not in category_params: category_params['category_help'] = ''
subparsers_category[category] = subparsers.add_parser(category, help=category_params['category_help'])
subparsers_action[category] = subparsers_category[category].add_subparsers()
# Split actions
if 'actions' in category_params:
for action, action_params in category_params['actions'].items():
if 'action_help' not in action_params: action_params['action_help'] = ''
parsers[category + '_' + action] = subparsers_action[category].add_parser(action, help=action_params['action_help'])
# Set the action s related function
parsers[category + '_' + action].set_defaults(
func=str_to_func('yunohost_' + category + '.'
+ category + '_' + action))
# Add arguments
if 'arguments' in action_params:
for arg_name, arg_params in action_params['arguments'].items():
arg_fullname = False
if 'password' in arg_params:
if arg_params['password']: is_password = True
del arg_params['password']
else: is_password = False
if 'full' in arg_params:
arg_names = [arg_name, arg_params['full']]
arg_fullname = arg_params['full']
del arg_params['full']
else: arg_names = [arg_name]
if 'ask' in arg_params:
require_input = True
if '-h' in sys.argv or '--help' in sys.argv:
require_input = False
if (category != sys.argv[1]) or (action != sys.argv[2]):
require_input = False
for name in arg_names:
if name in sys.argv[2:]: require_input = False
if require_input:
if is_password:
if os.isatty(1):
pwd1 = getpass.getpass(colorize(arg_params['ask'] + ': ', 'cyan'))
pwd2 = getpass.getpass(colorize('Retype ' + arg_params['ask'][0].lower() + arg_params['ask'][1:] + ': ', 'cyan'))
if pwd1 != pwd2:
raise YunoHostError(22, _("Passwords don't match"))
sys.exit(1)
else:
raise YunoHostError(22, _("Missing arguments") + ': ' + arg_name)
if arg_name[0] == '-': arg_extend = [arg_name, pwd1]
else: arg_extend = [pwd1]
else:
if os.isatty(1):
arg_value = raw_input(colorize(arg_params['ask'] + ': ', 'cyan'))
else:
raise YunoHostError(22, _("Missing arguments") + ': ' + arg_name)
if arg_name[0] == '-': arg_extend = [arg_name, arg_value]
else: arg_extend = [arg_value]
new_args.extend(arg_extend)
del arg_params['ask']
if 'pattern' in arg_params:
if (category == sys.argv[1]) and (action == sys.argv[2]):
if 'dest' in arg_params: name = arg_params['dest']
elif arg_fullname: name = arg_fullname[2:]
else: name = arg_name
name = name.replace('-', '_')
patterns[name] = arg_params['pattern']
del arg_params['pattern']
parsers[category + '_' + action].add_argument(*arg_names, **arg_params)
args = parsers['general'].parse_args(sys.argv.extend(new_args))
args_dict = vars(args)
for key, value in patterns.items():
validate(value, args_dict[key])
return args

155
yunohost.tac Executable file
View file

@ -0,0 +1,155 @@
# -*- mode: python -*-
import os
import sys
import gettext
import ldap
import yaml
import json
from twisted.python import log
from twisted.web.server import Site
from twisted.web.resource import IResource
from twisted.web.guard import HTTPAuthSessionWrapper, BasicCredentialFactory
from twisted.internet import reactor, defer
from twisted.cred.portal import IRealm, Portal
from twisted.cred.checkers import ICredentialsChecker
from twisted.cred.credentials import IUsernamePassword
from twisted.cred.error import UnauthorizedLogin
from zope.interface import implements
from txrestapi.resource import APIResource
from yunohost import YunoHostError, YunoHostLDAP, str_to_func, colorize, pretty_print_dict, display_error, validate, win, parse_dict
if not __debug__:
import traceback
gettext.install('YunoHost')
class LDAPHTTPAuth():
implements (ICredentialsChecker)
credentialInterfaces = IUsernamePassword,
def requestAvatarId(self, credentials):
try:
if credentials.username != "admin":
raise YunoHostError(22, _("Invalid username") + ': ' + credentials.username)
YunoHostLDAP(password=credentials.password)
return credentials.username
except Exception as e:
return defer.fail(UnauthorizedLogin())
class SimpleRealm(object):
implements(IRealm)
_api = None
def __init__(self, api):
self._api = api
def requestAvatar(self, avatarId, mind, *interfaces):
if IResource in interfaces:
return IResource, self._api, lambda: None
raise NotImplementedError()
action_dict = {}
def http_exec(request):
global win
dict = action_dict[request.method+' '+request.path]
if 'arguments' in dict: args = dict['arguments']
else: args = {}
for arg, params in args.items():
sanitized_key = arg.replace('-', '_')
if sanitized_key is not arg:
args[sanitized_key] = args[arg]
del args[arg]
arg = sanitized_key
if arg[0] == '_':
if 'nargs' not in params:
args[arg]['nargs'] = '*'
if 'full' in params:
new_key = params['full'][2:]
else:
new_key = arg[2:]
args[new_key] = args[arg]
del args[arg]
try:
validated_args = {}
for key, value in request.args.items():
if key in args:
# Validate args
if 'pattern' in args[key]: validate(args[key]['pattern'], value)
if 'nargs' not in args[key] or ('nargs' != '*' and 'nargs' != '+'): value = value[0]
if 'action' in args[key] and args[key]['action'] == 'store_true':
yes = ['true', 'True', 'yes', 'Yes']
value = value in yes
validated_args[key] = value
func = str_to_func(dict['function'])
with YunoHostLDAP(password=request.getPassword()):
result = func(**validated_args)
if result is None:
result = {}
if win:
result['win'] = win
win = []
if request.method == 'POST':
request.setResponseCode(201, 'Created')
elif request.method == 'DELETE':
request.setResponseCode(204, 'No Content')
else:
request.setResponseCode(200, 'OK')
except YunoHostError, error:
server_errors = [1, 111, 169]
client_errors = [13, 17, 22, 87, 122, 125, 167, 168]
if error.code in client_errors:
request.setResponseCode(400, 'Bad Request')
else:
request.setResponseCode(500, 'Internal Server Error')
result = { 'error' : error.message }
request.setHeader('Content-Type', 'application/json')
return json.dumps(result)
def main():
global action_dict
log.startLogging(sys.stdout)
api = APIResource()
with open('action_map.yml') as f:
action_map = yaml.load(f)
del action_map['general_arguments']
for category, category_params in action_map.items():
for action, action_params in category_params['actions'].items():
if 'help' not in action_params:
action_params['help'] = ''
if 'api' not in action_params:
action_params['api'] = 'GET /'+ category +'/'+ action
method, path = action_params['api'].split(' ')
api.register(method, path, http_exec)
action_dict[action_params['api']] = {
'function': 'yunohost_'+ category +'.'+ category +'_'+ action,
'help' : action_params['help']
}
if 'arguments' in action_params:
action_dict[action_params['api']]['arguments'] = action_params['arguments']
ldap_auth = LDAPHTTPAuth()
credentialFactory = BasicCredentialFactory("Restricted Area")
resource = HTTPAuthSessionWrapper(Portal(SimpleRealm(api), [ldap_auth]), [credentialFactory])
try:
with open('/etc/yunohost/installed') as f: pass
except IOError:
resource = APIResource()
resource.register('POST', '/postinstall', http_exec)
reactor.listenTCP(6767, Site(resource, timeout=None))
reactor.run()
if __name__ == '__main__':
main()

21
yunohost_backup.py Normal file
View file

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
import os
import sys
import json
import yaml
import glob
from yunohost import YunoHostError, YunoHostLDAP, validate, colorize, win_msg
def backup_init(helper=False):
"""
Init Tahoe-LAFS configuration
Keyword arguments:
helper -- Create a helper node rather than a "helped" one
Returns:
Win | Fail
"""
pass

View file

@ -25,9 +25,21 @@ def tools_ldapinit():
for rdn, attr_dict in ldap_map['parents'].items():
yldap.add(rdn, attr_dict)
for rdn, attr_dict in ldap_map['childs'].items():
for rdn, attr_dict in ldap_map['children'].items():
yldap.add(rdn, attr_dict)
try:
with open('/etc/yunohost/from_script') as f: pass
except IOError:
with open('sudo_ldap_scheme.yml') as f:
ldap_map = yaml.load(f)
for rdn, attr_dict in ldap_map['parents'].items():
yldap.add(rdn, attr_dict)
for rdn, attr_dict in ldap_map['children'].items():
yldap.add(rdn, attr_dict)
admin_dict = {
'cn': 'admin',