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

View file

@ -23,7 +23,7 @@ parents:
- organizationalUnit - organizationalUnit
- top - top
childs: children:
cn=admins,ou=groups: cn=admins,ou=groups:
cn: admins cn: admins
gidNumber: "4001" 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') gettext.install('YunoHost')
try: 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: except ImportError:
sys.stderr.write('Error: Yunohost CLI Require YunoHost lib\n') sys.stderr.write('Error: Yunohost CLI Require YunoHost lib\n')
sys.exit(1) 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(): def main():
""" """
Main instructions Main instructions

View file

@ -9,11 +9,15 @@ except ImportError:
sys.stderr.write('apt-get install python-ldap\n') sys.stderr.write('apt-get install python-ldap\n')
sys.exit(1) sys.exit(1)
import ldap.modlist as modlist import ldap.modlist as modlist
import yaml
import json import json
import re import re
import getpass import getpass
import random import random
import string import string
import argparse
import gettext
import getpass
if not __debug__: if not __debug__:
import traceback import traceback
@ -93,7 +97,7 @@ def win_msg(astr):
global win global win
if os.isatty(1): if os.isatty(1):
print('\n' + colorize(_("Success: "), 'green') + astr + '\n') print('\n' + colorize(_("Success: "), 'green') + astr + '\n')
else:
win.append(astr) win.append(astr)
@ -444,3 +448,112 @@ class YunoHostLDAP(Singleton):
else: else:
raise YunoHostError(17, _('Attribute already exists') + ' "' + attr + '=' + value + '"') raise YunoHostError(17, _('Attribute already exists') + ' "' + attr + '=' + value + '"')
return True 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,7 +25,19 @@ def tools_ldapinit():
for rdn, attr_dict in ldap_map['parents'].items(): for rdn, attr_dict in ldap_map['parents'].items():
yldap.add(rdn, attr_dict) 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) yldap.add(rdn, attr_dict)