mirror of
https://github.com/YunoHost/moulinette.git
synced 2024-09-03 20:06:31 +02:00
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:
commit
89f8cb2a58
17 changed files with 851 additions and 158 deletions
|
@ -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
|
||||||
|
|
|
@ -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
26
sudo_ldap_scheme.yml
Normal 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
3
txrestapi/.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
_trial_temp
|
||||||
|
txrestapi.egg-info
|
||||||
|
txrestapi/_trial_temp
|
146
txrestapi/README.rst
Normal file
146
txrestapi/README.rst
Normal 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
1
txrestapi/setup.cfg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
[egg_info]
|
26
txrestapi/setup.py
Normal file
26
txrestapi/setup.py
Normal 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: -*-
|
||||||
|
""",
|
||||||
|
)
|
1
txrestapi/txrestapi/__init__.py
Normal file
1
txrestapi/txrestapi/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
#
|
29
txrestapi/txrestapi/methods.py
Normal file
29
txrestapi/txrestapi/methods.py
Normal 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')
|
65
txrestapi/txrestapi/resource.py
Normal file
65
txrestapi/txrestapi/resource.py
Normal 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
|
7
txrestapi/txrestapi/service.py
Normal file
7
txrestapi/txrestapi/service.py
Normal 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()
|
194
txrestapi/txrestapi/tests.py
Normal file
194
txrestapi/txrestapi/tests.py
Normal 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
111
yunohost
|
@ -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
|
||||||
|
|
115
yunohost.py
115
yunohost.py
|
@ -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
155
yunohost.tac
Executable 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
21
yunohost_backup.py
Normal 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
|
|
@ -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)
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue