Merge remote-tracking branch 'origin/dev' into enh-python3

This commit is contained in:
Kay0u 2020-12-04 22:15:36 +01:00
commit bd27799283
No known key found for this signature in database
GPG key ID: AAFEEB16CFA2AE2D
44 changed files with 607 additions and 495 deletions

View file

@ -7,12 +7,19 @@ addons:
- slapd
matrix:
allow_failures:
- env: TOXENV=py37-pytest
- env: TOXENV=py37-lint
include:
- python: 3.5
env: TOXENV=py35
- python: 3.5
env: TOXENV=lint
- python: 3.6
- python: 2.7
env: TOXENV=py27-pytest
- python: 2.7
env: TOXENV=py27-lint
- python: 3.7
env: TOXENV=py37-pytest
- python: 3.7
env: TOXENV=py37-lint
- python: 3.7
env: TOXENV=format-check
- python: 3.5
env: TOXENV=docs

140
debian/changelog vendored
View file

@ -1,3 +1,105 @@
moulinette (4.1.0) testing; urgency=low
- [enh] Simplify interface initialization (Moulinette#245)
- [enh] Be able to return a raw HTTP response (Moulinette#255)
- [fix] Incorrect locale in some situations (Moulinette/d9fa6c7)
- [fix] Prevent installing moulinette 4.1 without Yunohost 4.1 (Moulinette/9609fe1)
- [fix] Error messages are not displayed in some situations (Moulinette/2501ecd)
- Update translations for French, Spanish, Italian, Portuguese, Czech (Moulinette#256)
Thanks to all contributors <3 ! (ppr, KaeruCT, Omnia89, roukydesbois, miloskroulik, Aleks, Kay0u, miloskroulik)
-- Kay0u <pierre@kayou.io> Thu, 03 Dec 2020 16:32:44 +0100
moulinette (4.0.3) stable; urgency=low
- Bump version number for stable release
-- Alexandre Aubin <alex.aubin@mailoo.org> Wed, 29 Jul 2020 17:00:00 +0200
moulinette (4.0.2~beta) testing; urgency=low
- Bump version number for beta release
-- Alexandre Aubin <alex.aubin@mailoo.org> Fri, 19 Jun 2020 15:33:29 +0200
moulinette (4.0.1~alpha) testing; urgency=low
- [fix] Get rid of legacy code which breaks postinstall on buster for some reason (ac83b10f)
- [fix] Remove legacy Breaks and Replaces (e49a47c7)
- [fix] Let's hash the password like we do in core during tests (0c78374e)
-- Alexandre Aubin <alex.aubin@mailoo.org> Fri, 05 Jun 2020 17:32:35 +0200
moulinette (3.8.1.3) stable; urgency=low
- Update authorship/maintainer information (7818f07)
- [i18n] Translations updated for Arabic, Catalan, French, Italian
Thanks to all contributors <3 ! (ButterflyOfFire, É. Gaspar, L. Noferini, ppr, xaloc33)
-- Alexandre Aubin <alex.aubin@mailoo.org> Mon, 27 Jul 2020 17:15:36 +0200
moulinette (3.8.1.2) stable; urgency=low
- [fix] locale parsing in some edge case
- [i18n] Translations updated for Chinese (Simplified), Catalan, French, Nepali, Occitan
Thanks to all contributors ! (Aleks, amirale qt, clecle226, Quentí, xaloc33)
-- Kay0u <pierre@kayou.io> Fri, 22 May 2020 07:58:19 +0000
moulinette (3.8.1.1) stable; urgency=low
Bumping version number for stable release
-- Kay0u <pierre@kayou.io> Wed, 20 May 2020 18:56:36 +0000
moulinette (3.8.1) testing; urgency=low
- [fix] Misc technical ux/debugging fixes (#242, #243, #244, 840f27d2)
- [fix] try to autorestart ldap when the server is down (#247)
- [i18n] Translations updated for Dutch, Esperanto, French, German, Nepali, Polish
Thanks to all contributors <3 ! (amirale qt, Bram, É. Gaspar, Kay0u, M. Döring, Zeik0s)
-- Alexandre Aubin <alex.aubin@mailoo.org> Sat, 09 May 2020 21:09:35 +0200
moulinette (3.8.0) testing; urgency=low
# Major stuff
- Simplify auth mechanism (#216)
- Add more tests (#230)
- Use Black in Moulinette (#220, 6f5daa0, 54b8cab)
# Minor technical stuff
- [fix] Don't display comment if argument is already set (#226)
- Don't miserably crash if async running can't read incoming message (06d8c48)
- Report the actual error when ldap fails (628ffc9)
# i18n
- Improve translations for Swedish, Dutch, Italian, Russian, Polish, Portuguese, Catalan, Spanish, Occitan, Nepali, Esperanto, Basque, Chinese (Simplified), Arabic, German, Hungarian, Greek, Turkish, Bengali (Bangladesh)
Thanks to all contributors ! (Aleks, Bram, ButterflyOfFire, Filip B., Jeroen F., Josué T., Kay0u, Quentí, Yifei D., amirale qt, decentral1se, Elie G., frju365, Romain R., xaloc33)
-- Kay0u <pierre@kayou.io> Thu, 09 Apr 2020 20:29:48 +0000
moulinette (3.7.1.1) stable; urgency=low
- [fix] Report actual errors when some LDAP operation fails to ease
debugging
-- Alexandre Aubin <alex.aubin@mailoo.org> Fri, 17 Apr 2020 17:00:00 +0000
moulinette (3.7.1) stable; urgency=low
- [enh] Lazy loading pytz for performances
-- Alexandre Aubin <alex.aubin@mailoo.org> Thu, 9 Apr 2020 14:55:00 +0000
moulinette (3.7.0.2) stable; urgency=low
Bumping version number for stable release
@ -8,7 +110,7 @@ moulinette (3.7.0.1) testing; urgency=low
- [fix] Slapd may crash if we try to update the LDAP with no change (moulinette#231)
Thanks to all contributors (Josue) <3 !
Thanks to all contributors (Josué) <3 !
-- Kay0u <pierre@kayou.io> Sun, 15 Mar 2020 16:09:25 +0000
@ -16,21 +118,21 @@ moulinette (3.7.0) testing; urgency=low
# ~ Major stuff
- [enh] Add group and permission mechanism ([Moulinette#189](https://github.com/YunoHost/moulinette/pull/189)
- [mod] Be able to customize prompt colors ([Moulinette/808f620](https://github.com/YunoHost/Moulinette/commit/808f620))
- [enh] Support app manifests in toml ([Moulinette#204](https://github.com/YunoHost/moulinette/pull/204), [Moulinette/55515cb](https://github.com/YunoHost/Moulinette/commit/55515cb))
- [enh] Quite a lot of messages improvements, string cleaning, language rework... ([Moulinette/599bec3](https://github.com/YunoHost/Moulinette/commit/599bec3), [Moulinette#208](https://github.com/YunoHost/moulinette/pull/208), [Moulinette#213](https://github.com/YunoHost/moulinette/pull/213), [Moulinette/b7d415d](https://github.com/YunoHost/Moulinette/commit/b7d415d), [Moulinette/a8966b8](https://github.com/YunoHost/Moulinette/commit/a8966b8), [Moulinette/fdf9a71](https://github.com/YunoHost/Moulinette/commit/fdf9a71), [Moulinette/d895ae3](https://github.com/YunoHost/Moulinette/commit/d895ae3), [Moulinette/bdf0a1c](https://github.com/YunoHost/Moulinette/commit/bdf0a1c))
- [enh] Add group and permission mechanism (#189)
- [mod] Be able to customize prompt colors (808f620)
- [enh] Support app manifests in toml (#204, 55515cb)
- [enh] Quite a lot of messages improvements, string cleaning, language rework... (599bec3, #208, #213, b7d415d, a8966b8, fdf9a71, d895ae3, bdf0a1c)
- [i18n] Improved translations for Catalan, Occitan, French, Arabic, Spanish, German, Norwegian Bokmål
# Smaller or pretty technical fix/enh
- [enh] Preparations for moulinette Python3 migration (Tox, Pytest and unit tests) ([Moulinette#203](https://github.com/YunoHost/moulinette/pull/203), [Moulinette#206](https://github.com/YunoHost/moulinette/pull/206), [Moulinette#207](https://github.com/YunoHost/moulinette/pull/207), [Moulinette#210](https://github.com/YunoHost/moulinette/pull/210), [Moulinette#211](https://github.com/YunoHost/moulinette/pull/211) [Moulinette#212](https://github.com/YunoHost/moulinette/pull/212), [Moulinette/2403ee1](https://github.com/YunoHost/Moulinette/commit/2403ee1), [Moulinette/69b0d49](https://github.com/YunoHost/Moulinette/commit/69b0d49), [Moulinette/49c749c](https://github.com/YunoHost/Moulinette/commit/49c749c), [Moulinette/2c84ee1](https://github.com/YunoHost/Moulinette/commit/2c84ee1), [Moulinette/cef72f7](https://github.com/YunoHost/Moulinette/commit/cef72f7))
- [enh] Add a write_to_yaml utility similar to write_to_json ([Moulinette/2e2e627](https://github.com/YunoHost/Moulinette/commit/2e2e627))
- [enh] Warn the user about long locks ([Moulinette#205](https://github.com/YunoHost/moulinette/pull/205))
- [mod] Tweak stuff about setuptools and moulinette deps? ([Moulinette/b739f27](https://github.com/YunoHost/Moulinette/commit/b739f27), [Moulinette/da00fc9](https://github.com/YunoHost/Moulinette/commit/da00fc9), [Moulinette/d8cbbb0](https://github.com/YunoHost/Moulinette/commit/d8cbbb0))
- [fix] Misc micro bugfixes or improvements ([Moulinette/83d9e77](https://github.com/YunoHost/Moulinette/commit/83d9e77))
- [doc] Fix doc building + add doc build tests with Tox ([Moulinette/f1ac5b8](https://github.com/YunoHost/Moulinette/commit/f1ac5b8), [Moulinette/df7d478](https://github.com/YunoHost/Moulinette/commit/df7d478), [Moulinette/74c8f79](https://github.com/YunoHost/Moulinette/commit/74c8f79), [Moulinette/bcf92c7](https://github.com/YunoHost/Moulinette/commit/bcf92c7), [Moulinette/af2c80c](https://github.com/YunoHost/Moulinette/commit/af2c80c), [Moulinette/d52a574](https://github.com/YunoHost/Moulinette/commit/d52a574), [Moulinette/307f660](https://github.com/YunoHost/Moulinette/commit/307f660), [Moulinette/dced104](https://github.com/YunoHost/Moulinette/commit/dced104), [Moulinette/ed3823b](https://github.com/YunoHost/Moulinette/commit/ed3823b))
- [enh] READMEs improvements ([Moulinette/1541b74](https://github.com/YunoHost/Moulinette/commit/1541b74), [Moulinette/ad1eeef](https://github.com/YunoHost/Moulinette/commit/ad1eeef))
- [enh] Preparations for moulinette Python3 migration (Tox, Pytest and unit tests) (#203, #206, #207, #210, #211 #212, 2403ee1, 69b0d49, 49c749c, 2c84ee1, cef72f7)
- [enh] Add a write_to_yaml utility similar to write_to_json (2e2e627)
- [enh] Warn the user about long locks (#205)
- [mod] Tweak stuff about setuptools and moulinette deps? (b739f27, da00fc9, d8cbbb0)
- [fix] Misc micro bugfixes or improvements (83d9e77)
- [doc] Fix doc building + add doc build tests with Tox (f1ac5b8, df7d478, 74c8f79, bcf92c7, af2c80c, d52a574, 307f660, dced104, ed3823b)
- [enh] READMEs improvements (1541b74, ad1eeef)
Thanks to all contributors <3 ! (accross all repo: Yunohost, Moulinette, SSOwat, Yunohost-admin) : advocatux, Aksel K., Aleks, Allan N., amirale qt, Armin P., Bram, ButterflyOfFire, Carles S. A., chema o. r., decentral1se, Emmanuel V., Etienne M., Filip B., Geoff M., htsr, Jibec, Josué, Julien J., Kayou, liberodark, ljf, lucaskev, Lukas D., madtibo, Martin D., Mélanie C., nr 458 h, pitfd, ppr, Quentí, sidddy, troll, tufek yamero, xaloc33, yalh76
@ -80,7 +182,7 @@ moulinette (3.5.1) testing; urgency=low
* [fix] Fix case where stdinfo is not provided in call_async_output (0a300e5)
* [i18n] Improve translation for Greek, Hungarian, Polish, Swedish, French, Catalan, Occitan
Thanks to all contributors (Aleks, ariasuni, Quenti, ppr, Xaloc) <3 !
Thanks to all contributors (Aleks, ariasuni, Quentí, ppr, Xaloc) <3 !
-- Alexandre Aubin <alex.aubin@mailoo.org> Wed, 03 Apr 2019 02:25:00 +0000
@ -88,7 +190,7 @@ moulinette (3.5.0) testing; urgency=low
* [i18n] Improve Russian and Chinese (Mandarin) translations
Contributors : n3uz, Алексей
Contributors : n3uz, Алексей
-- Alexandre Aubin <alex.aubin@mailoo.org> Wed, 13 Mar 2019 17:20:00 +0000
@ -201,7 +303,7 @@ moulinette (2.7.13) testing; urgency=low
* [i18n] Improve translations for Portugueuse, Occitan
* [enh] Add read_yaml util (#161)
Contributors : Bram, by0ne, Quent-in
Contributors : Bram, by0ne, Quentí
-- Alexandre Aubin <alex.aubin@mailoo.org> Mon, 28 May 2018 02:55:00 +0000
@ -219,7 +321,7 @@ moulinette (2.7.11) testing; urgency=low
* [fix] Avoid cases where get_cookie returns None
* [mod] Improve exception logging in ldap stuff
Thanks to all contributors (pitchum, Bram, ButteflyOfFire, J. Keerl, Matthieu, Jibec, David B, Quenti, bjarkan) <3 !
Thanks to all contributors (pitchum, Bram, ButteflyOfFire, J. Keerl, Matthieu, Jibec, David B, Quentí, bjarkan) <3 !
-- Alexandre Aubin <alex.aubin@mailoo.org> Tue, 01 May 2018 23:33:59 +0000
@ -291,7 +393,7 @@ moulinette (2.7.0) testing; urgency=low
* [enh] Show description of command in --help (#148)
* [i18n] Update French translation (#149)
Thanks to all contributors (Bram, Aleks, R. Cabaret) ! <3
Thanks to all contributors (Bram, Aleks, R. Cabaret) ! <3
-- Alexandre Aubin <alex.aubin@mailoo.org> Mon, 07 Aug 2017 13:04:21 -0400
@ -307,9 +409,9 @@ moulinette (2.6.0) testing; urgency=low
* [fix] Use ordered dict for the actionmap cache (#136)
* [fix] Show positional arguments first in --help / usage (#138)
* Update translations for Portuguese, German, Dutch
* Update translations for Portuguese, German, Dutch
Thanks to all contributors and translators ! (Trollken, frju, Fabien Gruber, Jeroen Keerl, Aleks)
Thanks to all contributors and translators ! (Trollken, frju, Fabien Gruber, Jeroen Keerl, Aleks)
-- Alexandre Aubin <alex.aubin@mailoo.org> Mon, 24 Apr 2017 12:44:23 -0400

14
debian/control vendored
View file

@ -1,7 +1,7 @@
Source: moulinette
Section: python
Priority: optional
Maintainer: Jérôme Lebleu <jerome.lebleu@mailoo.org>
Maintainer: YunoHost Contributors <contrib@yunohost.org>
Build-Depends: debhelper (>= 9), python (>= 2.7), dh-python, python-setuptools, python-psutil, python-all (>= 2.7)
Standards-Version: 3.9.6
X-Python-Version: >= 2.7
@ -18,12 +18,10 @@ Depends: ${misc:Depends}, ${python:Depends},
python-toml,
python-psutil,
python-tz
Replaces: yunohost-cli
Breaks: yunohost-cli
Breaks: yunohost (<< 4.1)
Description: prototype interfaces with ease in Python
The moulinette is a Python package that allows one to quickly and
easily prototype interfaces for your application. Each action can
be served through an HTTP API and from the command-line with a single
method.
Quickly and easily prototype interfaces for your application.
Each action can be served through an HTTP API and from the
command-line with a single method.
.
It was originally written for the YunoHost project.
Originally designed and written for the YunoHost project.

View file

@ -18,7 +18,6 @@
"invalid_password": "كلمة السر خاطئة",
"invalid_usage": "إستعمال غير صالح، إستخدم --help لعرض المساعدة",
"ldap_attribute_already_exists": "الخاصية '{attribute}' موجودة مسبقا و تحمل القيمة '{value}'",
"ldap_operation_error": "طرأ هناك خطأ أثناء عملية في LDAP",
"ldap_server_down": "لا يمكن الإتصال بخادم LDAP",
"logged_in": "مُتّصل",
"logged_out": "تم تسجيل خروجك",
@ -53,6 +52,9 @@
"corrupted_yaml": "قراءة مُشوّهة لنسق yaml مِن {ressource:s} (السبب : {error:s})",
"info": "معلومة:",
"warn_the_user_about_waiting_lock_again": "جارٍ الانتظار…",
"warn_the_user_that_lock_is_acquired": "لقد انتهى تنفيذ ذاك الأمر ، جارٍ إطلاق الأمر",
"warn_the_user_about_waiting_lock": "هناك أمر لـ YunoHost قيد التشغيل حاليا. في انتظار انتهاء تنفيذه قبل تشغيل التالي"
"warn_the_user_that_lock_is_acquired": "لقد انتهى تنفيذ ذاك الأمر للتوّ ، جارٍ تنفيذ هذا الأمر",
"warn_the_user_about_waiting_lock": "هناك أمر لـ YunoHost قيد التشغيل حاليا. في انتظار انتهاء تنفيذه قبل تشغيل التالي",
"ldap_server_is_down_restart_it": "إنّ خدمة LDAP غير مشغّلة ، نحن بصدد محاولة إعادة تشغيلها…",
"session_expired": "لقد انتهت مدة صلاحية الجلسة. رجاءً أعد الإستيثاق.",
"invalid_token": "إنّ الرمز المميز غير صالح - يرجى الإستيثاق"
}

View file

@ -18,7 +18,6 @@
"invalid_password": "Contrasenya invàlida",
"invalid_usage": "Utilització invàlida, utilitzeu --help per veure l'ajuda",
"ldap_attribute_already_exists": "L'atribut '{attribute}' ja existeix amb el valor '{value}'",
"ldap_operation_error": "Hi ha hagut un error durant l'operació de LDAP",
"ldap_server_down": "No s'ha pogut connectar amb el servidor LDAP",
"logged_in": "Sessió iniciada",
"logged_out": "Sessió tancada",
@ -40,21 +39,23 @@
"cannot_open_file": "No s'ha pogut obrir el fitxer {file:s} (motiu: {error:s})",
"cannot_write_file": "No s'ha pogut escriure el fitxer {file:s} (motiu: {error:s})",
"unknown_error_reading_file": "Error desconegut al intentar llegir el fitxer {file:s} (motiu: {error:s})",
"corrupted_json": "Json corrupte llegit des de {ressource:s} (motiu: {error:s})",
"corrupted_yaml": "Yaml corrupte llegit des de {ressource:s} (motiu: {error:s})",
"corrupted_json": "JSON corrupte llegit des de {ressource:s} (motiu: {error:s})",
"corrupted_yaml": "YAML corrupte llegit des de {ressource:s} (motiu: {error:s})",
"error_writing_file": "Error al escriure el fitxer {file:s}: {error:s}",
"error_removing": "Error al eliminar {path:s}: {error:s}",
"error_changing_file_permissions": "Error al canviar els permisos per {path:s}: {error:s}",
"invalid_url": "Url invàlid {url:s} (el lloc web existeix?)",
"invalid_url": "URL invàlid {url:s} (el lloc web existeix?)",
"download_ssl_error": "Error SSL al connectar amb {url:s}",
"download_timeout": "{url:s} ha tardat massa en respondre, s'ha deixat d'esperar.",
"download_unknown_error": "Error al baixar dades des de {url:s}: {error:s}",
"download_bad_status_code": "{url:s} ha retornat el codi d'estat {code:s}",
"command_unknown": "Ordre '{command:s}' desconegut ?",
"command_unknown": "Ordre '{command:s}' desconegut?",
"info": "Info:",
"corrupted_toml": "El fitxer TOML ha estat corromput en la lectura des de {ressource:s} (motiu: {error:s})",
"warn_the_user_about_waiting_lock": "Hi ha una altra ordre de YunoHost en execució, s'executarà aquesta ordre un cop l'anterior hagi acabat",
"warn_the_user_about_waiting_lock_again": "Encara en espera…",
"warn_the_user_that_lock_is_acquired": "l'altra ordre tot just ha acabat, ara s'executarà aquesta ordre",
"invalid_token": "Testimoni no vàlid - torneu-vos a autenticar"
"warn_the_user_that_lock_is_acquired": "L'altra ordre tot just ha acabat, ara s'executarà aquesta ordre",
"invalid_token": "Testimoni no vàlid - torneu-vos a autenticar",
"ldap_server_is_down_restart_it": "El servei LDAP està caigut, s'està intentant tornar-lo a engegar…",
"session_expired": "La sessió a expirat. Torneu-vos a autenticar."
}

View file

@ -14,14 +14,13 @@
"folder_exists": "目录已存在:{path}",
"folder_not_exist": "目录不存在",
"info": "信息:",
"instance_already_running": "实例已正在运行",
"instance_already_running": "已经有一个YunoHost操作正在运行。 请等待它完成再运行另一个。",
"invalid_argument": "参数错误{argument}{error}",
"invalid_password": "密码错误",
"invalid_usage": "用法错误,输入 --help 查看帮助信息",
"ldap_attribute_already_exists": "参数{attribute}已赋值{value}",
"ldap_operation_error": "LDAP操作时发生了错误",
"ldap_server_down": "无法连接LDAP服务器",
"logged_in": "登录成功",
"logged_in": "登录",
"logged_out": "登出",
"not_logged_in": "您未登录",
"operation_interrupted": "操作中断",
@ -31,7 +30,7 @@
"server_already_running": "服务已运行在指定端口",
"success": "成功!",
"unable_authenticate": "认证失败",
"unable_retrieve_session": "获取会话失败",
"unable_retrieve_session": "由于“ {exception}”,无法检索会话",
"unknown_group": "未知组{group}",
"unknown_user": "未知用户{user}",
"values_mismatch": "值不匹配",
@ -39,16 +38,23 @@
"websocket_request_expected": "期望一个WebSocket请求",
"cannot_open_file": "不能打开文件{file:s}(原因:{error:s}",
"cannot_write_file": "写入文件{file:s}失败(原因:{error:s}",
"unknown_error_reading_file": "尝试读取文件{file:s}时发生错误",
"corrupted_json": "json数据{ressource:s}读取失败(原因:{error:s}",
"corrupted_yaml": "读取yaml文件{ressource:s}失败(原因:{error:s}",
"unknown_error_reading_file": "尝试读取文件{files}时发生未知错误(原因:{errors}",
"corrupted_json": "从{ressource:s}读取的JSON损坏(原因:{error:s}",
"corrupted_yaml": "从{ressource:s}读取的YMAL损坏(原因:{error:s}",
"error_writing_file": "写入文件{file:s}失败:{error:s}",
"error_removing": "删除路径{path:s}失败:{error:s}",
"error_changing_file_permissions": "目录{path:s}权限修改失败:{error:s}",
"invalid_url": "url{url:s}无效site是否存在",
"invalid_url": "URL{url:s}无效site是否存在",
"download_ssl_error": "连接{url:s}时发生SSL错误",
"download_timeout": "{url:s}响应超时,放弃。",
"download_unknown_error": "下载{url:s}失败:{error:s}",
"download_bad_status_code": "{url:s}返回状态码:{code:s}",
"command_unknown": "未知命令:{command:s}"
"command_unknown": "命令'{command:s}'未知?",
"warn_the_user_that_lock_is_acquired": "另一个命令刚刚完成,现在启动此命令",
"warn_the_user_about_waiting_lock_again": "还在等...",
"warn_the_user_about_waiting_lock": "目前正在运行另一个YunoHost命令我们在运行此命令之前等待它完成",
"corrupted_toml": "从{ressources}读取的TOML损坏原因{errors}",
"invalid_token": "令牌无效-请进行身份验证",
"ldap_server_is_down_restart_it": "LDAP服务已下线正在尝试重启服务……",
"session_expired": "会话已过期。请重新进行身份验证。"
}

3
locales/cs.json Normal file
View file

@ -0,0 +1,3 @@
{
"password": "Heslo"
}

View file

@ -16,7 +16,6 @@
"invalid_password": "Passwort falsch",
"invalid_usage": "Falscher Aufruf, verwende --help für den Hilfstext",
"ldap_attribute_already_exists": "Attribute existieren bereits: '{attribute}={value}'",
"ldap_operation_error": "Ein Fehler trat während der LDAP Abfrage auf",
"ldap_server_down": "LDAP-Server nicht erreichbar",
"logged_in": "Angemeldet",
"logged_out": "Abgemeldet",
@ -37,5 +36,26 @@
"deprecated_command_alias": "'{prog} {old}' ist veraltet und wird bald entfernt werden, benutze '{prog} {new}' stattdessen",
"unknown_group": "Gruppe '{group}' ist unbekannt",
"unknown_user": "Benutzer '{user}' ist unbekannt",
"info": "Info:"
"info": "Info:",
"invalid_token": "Ungültiger Token - bitte authentifizieren",
"corrupted_json": "Beschädigtes JSON gelesen von {ressource:s} (reason: {error:s})",
"unknown_error_reading_file": "Unbekannter Fehler beim Lesen der Datei {file:s} (reason: {error:s})",
"cannot_write_file": "Kann Datei {file:s} nicht schreiben (reason: {error:s})",
"cannot_open_file": "Kann Datei {file:s} nicht öffnen (reason: {error:s})",
"corrupted_yaml": "Beschädigtes YAML gelesen von {ressource:s} (reason: {error:s})",
"warn_the_user_that_lock_is_acquired": "Der andere Befehl wurde gerade abgeschlossen, starte jetzt diesen Befehl",
"warn_the_user_about_waiting_lock_again": "Immer noch wartend...",
"warn_the_user_about_waiting_lock": "Ein anderer YunoHost Befehl läuft gerade, wir warten bis er fertig ist, bevor dieser laufen kann",
"command_unknown": "Befehl '{command:s}' unbekannt?",
"download_bad_status_code": "{url:s} lieferte folgende(n) Status Code(s) {code:s}",
"download_unknown_error": "Fehler beim Herunterladen von Daten von {url:s}: {error:s}",
"download_timeout": "{url:s} brauchte zu lange zum Antworten, hab aufgegeben.",
"download_ssl_error": "SSL Fehler beim Verbinden zu {url:s}",
"invalid_url": "Ungültige URL {url:s} (existiert diese Seite?)",
"error_changing_file_permissions": "Fehler beim Ändern der Berechtigungen für {path:s}: {error:s}",
"error_removing": "Fehler beim Entfernen {path:s}: {error:s}",
"error_writing_file": "Fehler beim Schreiben von Datei {file:s}: {error:s}",
"corrupted_toml": "Beschädigtes TOML gelesen von {ressource:s} (reason: {error:s})",
"ldap_server_is_down_restart_it": "Der LDAP-Dienst wurde angehalten. Es wird versucht, ihn erneut zu starten...",
"session_expired": "Die Sitzung ist abgelaufen. Bitte neuauthentifizieren."
}

View file

@ -19,7 +19,6 @@
"invalid_token": "Invalid token - please authenticate",
"invalid_usage": "Invalid usage, pass --help to see help",
"ldap_attribute_already_exists": "Attribute '{attribute}' already exists with value '{value}'",
"ldap_operation_error": "An error occurred during LDAP '{action}' operation",
"ldap_server_down": "Unable to reach LDAP server",
"logged_in": "Logged in",
"logged_out": "Logged out",
@ -32,6 +31,7 @@
"success": "Success!",
"unable_authenticate": "Unable to authenticate",
"unable_retrieve_session": "Unable to retrieve the session because '{exception}'",
"session_expired": "The session expired. Please re-authenticate.",
"unknown_group": "Unknown '{group}' group",
"unknown_user": "Unknown '{user}' user",
"values_mismatch": "Values don't match",
@ -41,19 +41,20 @@
"cannot_open_file": "Could not open file {file:s} (reason: {error:s})",
"cannot_write_file": "Could not write file {file:s} (reason: {error:s})",
"unknown_error_reading_file": "Unknown error while trying to read file {file:s} (reason: {error:s})",
"corrupted_json": "Corrupted json read from {ressource:s} (reason: {error:s})",
"corrupted_yaml": "Corrupted yaml read from {ressource:s} (reason: {error:s})",
"corrupted_toml": "Corrupted toml read from {ressource:s} (reason: {error:s})",
"error_writing_file": "Error when writing file {file:s}: {error:s}",
"corrupted_json": "Corrupted JSON read from {ressource:s} (reason: {error:s})",
"corrupted_yaml": "Corrupted YAML read from {ressource:s} (reason: {error:s})",
"corrupted_toml": "Corrupted TOML read from {ressource:s} (reason: {error:s})",
"error_writing_file": "Error when writing file {file:s}: {error:s}",
"error_removing": "Error when removing {path:s}: {error:s}",
"error_changing_file_permissions": "Error when changing permissions for {path:s}: {error:s}",
"invalid_url": "Invalid url {url:s} (does this site exists?)",
"invalid_url": "Invalid URL {url:s} (does this site exists?)",
"download_ssl_error": "SSL error when connecting to {url:s}",
"download_timeout": "{url:s} took too long to answer, gave up.",
"download_unknown_error": "Error when downloading data from {url:s}: {error:s}",
"download_bad_status_code": "{url:s} returned status code {code:s}",
"command_unknown": "Command '{command:s}' unknown ?",
"command_unknown": "Command '{command:s}' unknown?",
"warn_the_user_about_waiting_lock": "Another YunoHost command is running right now, we are waiting for it to finish before running this one",
"warn_the_user_about_waiting_lock_again": "Still waiting...",
"warn_the_user_that_lock_is_acquired": "the other command just completed, now starting this command"
"warn_the_user_that_lock_is_acquired": "The other command just completed, now starting this command",
"ldap_server_is_down_restart_it": "The LDAP service is down, attempt to restart it..."
}

View file

@ -1,7 +1,7 @@
{
"password": "Pasvorto",
"colon": "{}: ",
"warn_the_user_that_lock_is_acquired": "la alia komando nur kompletigas, nun komencante ĉi tiun komandon",
"warn_the_user_that_lock_is_acquired": "la alia komando ĵus kompletigis, nun komencante ĉi tiun komandon",
"warn_the_user_about_waiting_lock_again": "Ankoraŭ atendanta...",
"warn_the_user_about_waiting_lock": "Alia komando de YunoHost funkcias ĝuste nun, ni atendas, ke ĝi finiĝos antaŭ ol funkcii ĉi tiu",
"command_unknown": "Komando '{command:s}' nekonata?",
@ -34,7 +34,6 @@
"not_logged_in": "Vi ne estas ensalutinta",
"logged_in": "Ensalutinta",
"ldap_server_down": "Ne eblas atingi la servilon LDAP",
"ldap_operation_error": "Eraro okazis dum LDAP-operacio",
"ldap_attribute_already_exists": "Atributo '{attribute}' jam ekzistas kun valoro '{value}'",
"invalid_usage": "Nevalida uzado, preterpase '--help' por vidi helpon",
"invalid_password": "Nevalida pasvorto",

View file

@ -18,7 +18,6 @@
"invalid_password": "Contraseña no válida",
"invalid_usage": "Uso no válido, utilice --help para ver la ayuda",
"ldap_attribute_already_exists": "El atributo «{attribute}» ya existe con el valor «{value}»",
"ldap_operation_error": "Ha ocurrido un error durante la operación de LDAP",
"ldap_server_down": "No se pudo conectar con el servidor LDAP",
"logged_in": "Sesión iniciada",
"logged_out": "Sesión cerrada",
@ -40,21 +39,23 @@
"cannot_open_file": "No se pudo abrir el archivo {file:s} (motivo: {error:s})",
"cannot_write_file": "No se pudo escribir el archivo {file:s} (motivo: {error:s})",
"unknown_error_reading_file": "Error desconocido al intentar leer el archivo {file:s} (motivo: {error:s})",
"corrupted_json": "Lectura corrupta de Json desde {ressource:s} (motivo: {error:s})",
"corrupted_json": "Lectura corrupta de JSON desde {ressource:s} (motivo: {error:s})",
"error_writing_file": "Error al escribir el archivo {file:s}: {error:s}",
"error_removing": "Error al eliminar {path:s}: {error:s}",
"error_changing_file_permissions": "Error al cambiar los permisos para {path:s}: {error:s}",
"invalid_url": "Url no válida {url:s} (¿Existe este sitio?)",
"invalid_url": "URL inválida {url:s} (¿Existe este sitio?)",
"download_ssl_error": "Error SSL al conectar con {url:s}",
"download_timeout": "{url:s} tardó demasiado en responder, abandono.",
"download_unknown_error": "Error al descargar datos desde {url:s} : {error:s}",
"download_bad_status_code": "{url:s} devolvió el código de estado {code:s}",
"command_unknown": "¿Orden «{command:s}» desconocida?",
"corrupted_yaml": "Lectura corrupta de yaml desde {ressource:s} (motivo: {error:s})",
"corrupted_yaml": "Lectura corrupta de YAML desde {ressource:s} (motivo: {error:s})",
"info": "Información:",
"corrupted_toml": "Lectura corrupta de TOML desde {ressource:s} (motivo: {error:s})",
"warn_the_user_that_lock_is_acquired": "la otra orden recién terminó, iniciando esta orden ahora",
"warn_the_user_that_lock_is_acquired": "La otra orden recién terminó, iniciando esta orden ahora",
"warn_the_user_about_waiting_lock_again": "Aún esperando...",
"warn_the_user_about_waiting_lock": "Otra orden de YunoHost se está ejecutando ahora, estamos esperando a que termine antes de ejecutar esta",
"invalid_token": "Token invalido - vuelva a autenticarte"
"invalid_token": "Token invalido - vuelva a autenticarte",
"ldap_server_is_down_restart_it": "El servicio LDAP está caído, intentando reiniciarlo...",
"session_expired": "La sesión expiró. Por favor autenticarse de nuevo."
}

View file

@ -18,7 +18,6 @@
"invalid_password": "Mot de passe incorrect",
"invalid_usage": "Utilisation erronée, utilisez --help pour accéder à laide",
"ldap_attribute_already_exists": "Lattribut '{attribute}' existe déjà avec la valeur suivante : '{value}'",
"ldap_operation_error": "Une erreur est survenue lors de lopération LDAP",
"ldap_server_down": "Impossible datteindre le serveur LDAP",
"logged_in": "Connecté",
"logged_out": "Déconnecté",
@ -55,6 +54,8 @@
"corrupted_toml": "Fichier TOML corrompu en lecture depuis {ressource:s} (cause : {error:s})",
"warn_the_user_about_waiting_lock": "Une autre commande YunoHost est actuellement en cours, nous attendons qu'elle se termine avant de démarrer celle là",
"warn_the_user_about_waiting_lock_again": "Toujours en attente...",
"warn_the_user_that_lock_is_acquired": "l'autre commande vient de se terminer, lancement de cette commande",
"invalid_token": "Jeton non valide - veuillez vous authentifier"
"warn_the_user_that_lock_is_acquired": "La commande précédente vient de se terminer, lancement de cette nouvelle commande",
"invalid_token": "Jeton non valide - veuillez vous authentifier",
"ldap_server_is_down_restart_it": "Le service LDAP est arrêté, nous tentons de le redémarrer...",
"session_expired": "La session a expiré. Merci de vous ré-authentifier."
}

View file

@ -18,7 +18,6 @@
"invalid_password": "अवैध पासवर्ड",
"invalid_usage": "अवैध उपयोग, सहायता देखने के लिए --help साथ लिखे।",
"ldap_attribute_already_exists": "'{attribute}' तर्क पहले इस वैल्यू '{value}' से मौजूद है।",
"ldap_operation_error": "LDAP ऑपरेशन के दौरान त्रुटि हो गई है।",
"ldap_server_down": "LDAP सर्वर तक पहुंचने में असमर्थ।",
"logged_in": "लोग्ड इन",
"logged_out": "लॉग आउट",

View file

@ -20,7 +20,6 @@
"invalid_password": "Password non valida",
"invalid_usage": "Utilizzo non valido, usa --help per vedere l'aiuto",
"ldap_attribute_already_exists": "L'attributo '{attribute}' esiste già con valore '{value}'",
"ldap_operation_error": "Si è verificato un errore durante l'operazione LDAP",
"ldap_server_down": "Impossibile raggiungere il server LDAP",
"logged_in": "Connesso",
"not_logged_in": "Non hai effettuato l'accesso",
@ -40,21 +39,23 @@
"cannot_open_file": "Impossibile aprire il file {file:s} (motivo: {error:s})",
"cannot_write_file": "Impossibile scrivere il file {file:s} (motivo: {error:s})",
"unknown_error_reading_file": "Errore sconosciuto durante il tentativo di leggere il file {file:s} (motivo: {errore:s})",
"corrupted_json": "Lettura json corrotta da {ressource:s} (motivo: {error:s})",
"corrupted_yaml": "Lettura yaml corrotta da {ressource:s} (motivo: {error:s})",
"corrupted_json": "Lettura JSON corrotta da {resource:s} (motivo: {error:s})",
"corrupted_yaml": "Lettura YAML corrotta da {resource:s} (motivo: {error:s})",
"error_writing_file": "Errore durante la scrittura del file {file:s}: {error:s}",
"error_removing": "Errore durante la rimozione {path:s}: {error:s}",
"error_changing_file_permissions": "Errore durante il cambio di permessi per {path:s}: {error:s}",
"invalid_url": "URL non valido {url:s} (questo sito esiste?)",
"invalid_url": "URL non valido {url:s} (il sito esiste?)",
"download_ssl_error": "Errore SSL durante la connessione a {url:s}",
"download_timeout": "{url:s} ci ha messo troppo a rispondere, abbandonato.",
"download_unknown_error": "Errore durante il download di dati da {url:s} : {error:s}",
"download_bad_status_code": "{url:s} ha restituito il codice di stato {code:s}",
"command_unknown": "Comando '{command:s}' sconosciuto ?",
"command_unknown": "Comando '{command:s}' sconosciuto?",
"info": "Info:",
"warn_the_user_that_lock_is_acquired": "l'altro comando è appena completato, ora avvia questo comando",
"warn_the_user_that_lock_is_acquired": "L'altro comando è appena completato, ora avvio questo comando",
"warn_the_user_about_waiting_lock_again": "Sto ancora aspettando ...",
"warn_the_user_about_waiting_lock": "Un altro comando YunoHost è in esecuzione in questo momento, stiamo aspettando che finisca prima di eseguire questo",
"corrupted_toml": "Toml corrotto da {ressource:s} (motivo: {errore:s})",
"invalid_token": "Token non valido: autenticare"
"corrupted_toml": "TOML corrotto da {ressource:s} (motivo: {errore:s})",
"invalid_token": "Token non valido: autenticare",
"session_expired": "La sessione è terminata. Sei pregato di autenticarti nuovamente.",
"ldap_server_is_down_restart_it": "Il servizio LDAP è terminato, provo a riavviarlo..."
}

View file

@ -1 +1,11 @@
{}
{
"logged_out": "लग आउट",
"password": "पासवर्ड",
"deprecated_command_alias": "'{prog} {old}' अस्वीकृत गरिएको छ र भविष्यमा हटाइनेछ, यसको सट्टा '{prog} {new}' प्रयोग गर्नुहोस्।",
"deprecated_command": "'{prog} {command}' अस्वीकृत गरिएको छ र भविष्यमा हटाइनेछ",
"confirm": "कन्फर्म {prompt}",
"colon": "{}: ",
"authentication_required_long": "यस कार्य गर्नको लागि प्रमाणीकरण आवाश्यक हुन्छ",
"authentication_required": "प्रमाणीकरण आवाश्यक छ",
"argument_required": "तर्क '{argument}' आवश्यक छ"
}

View file

@ -16,7 +16,6 @@
"invalid_password": "Ongeldig wachtwoord",
"invalid_usage": "Ongeldig gebruik, doe --help om de hulptekst te lezen",
"ldap_attribute_already_exists": "Attribuut '{attribute}' bestaat al met waarde '{value}'",
"ldap_operation_error": "Er is een fout opgetreden bij het uitvoeren van LDAP operatie",
"ldap_server_down": "Kan LDAP server niet bereiken",
"logged_in": "Ingelogd",
"logged_out": "Uitgelogd",
@ -50,7 +49,7 @@
"download_unknown_error": "Fout tijdens het downloaden van data van {url:s}: {error:s}",
"download_bad_status_code": "{url:s} stuurt status code {code:s}",
"command_unknown": "Opdracht '{command:s}' ongekend ?",
"warn_the_user_that_lock_is_acquired": "het andere commando is net voltooid, starten van dit commando",
"warn_the_user_that_lock_is_acquired": "de andere opdracht is zojuist voltooid en start nu deze opdracht",
"warn_the_user_about_waiting_lock_again": "Nog steeds aan het wachten...",
"warn_the_user_about_waiting_lock": "Een ander YunoHost commando wordt uitgevoerd, we wachten tot het gedaan is alovrens dit te starten",
"corrupted_toml": "Ongeldige TOML werd gelezen op {ressource:s} (reason: {error:s})",

View file

@ -31,7 +31,6 @@
"warning": "Atencion:",
"invalid_usage": "Usatge invalid, utilizatz --help per accedir a lajuda",
"ldap_attribute_already_exists": "Latribut « {attribute} » existís ja amb la valor: {value}",
"ldap_operation_error": "Una error ses producha pendent loperacion LDAP",
"operation_interrupted": "Operacion interrompuda",
"server_already_running": "Un servidor es ja en execucion sus aqueste pòrt",
"success": "Capitada!",
@ -56,5 +55,7 @@
"warn_the_user_about_waiting_lock": "Una autra comanda YunoHost es en execucion, sèm a esperar quacabe abans daviar aquesta daquí",
"warn_the_user_about_waiting_lock_again": "Encara en espèra…",
"warn_the_user_that_lock_is_acquired": "lautra comanda ven dacabar, ara lançament daquesta comanda",
"invalid_token": "Geton invalid - volgatz vos autentificar"
"invalid_token": "Geton invalid - volgatz vos autentificar",
"ldap_server_is_down_restart_it": "Lo servici LDAP ses atudat, ensajam de lo reaviar…",
"session_expired": "La session a expirat. Tornatz vos autentificar."
}

View file

@ -1,7 +1,7 @@
{
"logged_out": "Wylogowano",
"password": "hasło",
"warn_the_user_that_lock_is_acquired": "drugie polecenie właśnie się zakończyło, teraz uruchamia to polecenie",
"password": "Hasło",
"warn_the_user_that_lock_is_acquired": "drugie polecenie właśnie się zakończyło, teraz uruchamiając to polecenie",
"warn_the_user_about_waiting_lock_again": "Wciąż czekam...",
"warn_the_user_about_waiting_lock": "Kolejne polecenie YunoHost jest teraz uruchomione, czekamy na jego zakończenie przed uruchomieniem tego",
"command_unknown": "Polecenie „{command:s}” jest nieznane?",
@ -34,7 +34,6 @@
"not_logged_in": "Nie jesteś zalogowany",
"logged_in": "Zalogowany",
"ldap_server_down": "Nie można połączyć się z serwerem LDAP",
"ldap_operation_error": "Wystąpił błąd podczas operacji LDAP",
"ldap_attribute_already_exists": "Atrybut „{attribute}” już istnieje z wartością „{value}”",
"invalid_usage": "Nieprawidłowe użycie. Przejdź --help, aby wyświetlić pomoc",
"invalid_token": "Nieprawidłowy token - proszę uwierzytelnić",

View file

@ -15,7 +15,6 @@
"invalid_password": "Senha incorreta",
"invalid_usage": "Uso invalido, utilizar --help para ver a ajuda",
"ldap_attribute_already_exists": "O atributo '{attribute}' já existe com valor '{value}'",
"ldap_operation_error": "Um erro ocorreu durante a operação LDAP",
"ldap_server_down": "Não foi possível comunicar com o servidor LDAP",
"logged_in": "Sessão iniciada",
"logged_out": "Sessão terminada",
@ -43,18 +42,20 @@
"error_writing_file": "Erro ao gravar arquivo {file:s}: {error:s}",
"error_removing": "Erro ao remover {path:s}: {error:s}",
"error_changing_file_permissions": "Erro ao alterar as permissões para {path:s}: {error:s}",
"invalid_url": "URL inválida {url:s} (does this site exists ?)",
"invalid_url": "URL inválida {url:s} (Esse site existe ?)",
"download_ssl_error": "Erro de SSL ao conectar-se a {url:s}",
"download_timeout": "{url:s} demorou muito para responder, desistiu.",
"download_unknown_error": "Erro quando baixando os dados de {url:s} : {error:s}",
"download_bad_status_code": "{url:s} retornou o código de status {code:s}",
"command_unknown": "Comando '{command:s}' desconhecido ?",
"corrupted_json": "Json corrompido lido do {ressource:s} (motivo: {error:s})",
"corrupted_yaml": "Yaml corrompido lido do {ressource:s} (motivo: {error:s})",
"warn_the_user_that_lock_is_acquired": "o outro comando acabou de concluir, agora iniciando este comando",
"corrupted_json": "JSON corrompido lido do {ressource:s} (motivo: {error:s})",
"corrupted_yaml": "YAML corrompido lido do {ressource:s} (motivo: {error:s})",
"warn_the_user_that_lock_is_acquired": "O outro comando acabou de concluir, agora iniciando este comando",
"warn_the_user_about_waiting_lock_again": "Ainda esperando...",
"warn_the_user_about_waiting_lock": "Outro comando YunoHost está sendo executado agora, estamos aguardando o término antes de executar este",
"corrupted_toml": "Toml corrompido lido em {ressource:s} (motivo: {error:s})",
"corrupted_toml": "TOML corrompido lido em {ressource:s} (motivo: {error:s})",
"invalid_token": "Token inválido - autentique",
"info": "Informações:"
"info": "Informações:",
"ldap_server_is_down_restart_it": "O serviço LDAP esta caído, tentando reiniciá-lo...",
"session_expired": "A sessão expirou. Se autentique de novo por favor."
}

View file

@ -41,7 +41,6 @@
"download_timeout": "Превышено время ожидания ответа от {url:s}.",
"download_unknown_error": "Ошибка при загрузке данных с {url:s} : {error:s}",
"instance_already_running": "Операция YunoHost уже запущена. Пожалуйста, подождите, пока он закончится, прежде чем запускать другой.",
"ldap_operation_error": "Ошибка в процессе работы LDAP",
"root_required": "Чтобы выполнить это действие, вы должны иметь права root",
"corrupted_json": "Повреждённый json получен от {ressource:s} (причина: {error:s})",
"command_unknown": "Команда '{command:s}' неизвестна ?",

View file

@ -49,7 +49,6 @@
"unable_retrieve_session": "Det gick inte att hämta sessionen eftersom '{exception}'",
"unable_authenticate": "Det går inte att verifiera",
"ldap_server_down": "Det går inte att nå LDAP-servern",
"ldap_operation_error": "Ett fel inträffade under LDAP-drift",
"invalid_usage": "Ogiltig användning, pass --help för att se hjälp",
"invalid_token": "Ogiltigt token - verifiera",
"instance_already_running": "Det finns redan en YunoHost-operation. Vänta tills den är klar innan du kör en annan.",

View file

@ -11,7 +11,6 @@
"invalid_argument": "Geçersiz argüman '{argument}': {error}",
"invalid_password": "Geçersiz parola",
"ldap_attribute_already_exists": "'{attribute}={value}' özelliği zaten mevcut",
"ldap_operation_error": "LDAP işlemi sırasında hata oluştu",
"ldap_server_down": "LDAP sunucusuna erişilemiyor",
"logged_in": "Giriş yapıldı",
"logged_out": ıkış yapıldı",

View file

@ -1,7 +1,6 @@
# -*- coding: utf-8 -*-
from moulinette.core import (
init_interface,
MoulinetteError,
MoulinetteSignals,
Moulinette18n,
@ -9,8 +8,7 @@ from moulinette.core import (
from moulinette.globals import init_moulinette_env
__title__ = "moulinette"
__version__ = "0.1"
__author__ = ["Kload", "jlebleu", "titoko", "beudbeud", "npze"]
__author__ = ["Yunohost Contributors"]
__license__ = "AGPL 3.0"
__credits__ = """
Copyright (C) 2014 YUNOHOST.ORG
@ -73,85 +71,57 @@ def init(logging_config=None, **kwargs):
# Easy access to interfaces
def api(
namespaces, host="localhost", port=80, routes={}, use_websocket=True, use_cache=True
):
def api(host="localhost", port=80, routes={}):
"""Web server (API) interface
Run a HTTP server with the moulinette for an API usage.
Keyword arguments:
- namespaces -- The list of namespaces to use
- host -- Server address to bind to
- port -- Server port to bind to
- routes -- A dict of additional routes to add in the form of
{(method, uri): callback}
- use_websocket -- Serve via WSGI to handle asynchronous responses
- use_cache -- False if it should parse the actions map file
instead of using the cached one
"""
from moulinette.interfaces.api import Interface as Api
try:
moulinette = init_interface(
"api",
kwargs={"routes": routes, "use_websocket": use_websocket},
actionsmap={"namespaces": namespaces, "use_cache": use_cache},
)
moulinette.run(host, port)
Api(routes=routes).run(host, port)
except MoulinetteError as e:
import logging
logging.getLogger(namespaces[0]).error(e.strerror)
return e.errno if hasattr(e, "errno") else 1
logging.getLogger(logging.main_logger).error(e.strerror)
return 1
except KeyboardInterrupt:
import logging
logging.getLogger(namespaces[0]).info(m18n.g("operation_interrupted"))
logging.getLogger(logging.main_logger).info(m18n.g("operation_interrupted"))
return 0
def cli(
namespaces,
args,
use_cache=True,
output_as=None,
password=None,
timeout=None,
parser_kwargs={},
):
def cli(args, top_parser, output_as=None, timeout=None):
"""Command line interface
Execute an action with the moulinette from the CLI and print its
result in a readable format.
Keyword arguments:
- namespaces -- The list of namespaces to use
- args -- A list of argument strings
- use_cache -- False if it should parse the actions map file
instead of using the cached one
- output_as -- Output result in another format, see
moulinette.interfaces.cli.Interface for possible values
- password -- The password to use in case of authentication
- parser_kwargs -- A dict of arguments to pass to the parser
class at construction
- top_parser -- The top parser used to build the ActionsMapParser
"""
from moulinette.interfaces.cli import Interface as Cli
try:
moulinette = init_interface(
"cli",
actionsmap={
"namespaces": namespaces,
"use_cache": use_cache,
"parser_kwargs": parser_kwargs,
},
load_only_category = args[0] if args and not args[0].startswith("-") else None
Cli(top_parser=top_parser, load_only_category=load_only_category).run(
args, output_as=output_as, timeout=timeout
)
moulinette.run(args, output_as=output_as, password=password, timeout=timeout)
except MoulinetteError as e:
import logging
logging.getLogger(namespaces[0]).error(e.strerror)
logging.getLogger(logging.main_logger).error(e.strerror)
return 1
return 0

View file

@ -4,7 +4,28 @@ import os
import re
import logging
import yaml
import pickle as pickle
import glob
import sys
if sys.version_info[0] == 3:
# python 3
import pickle as pickle
else:
# python 2
import cPickle as pickle
import codecs
import warnings
def open(file, mode='r', buffering=-1, encoding=None,
errors=None, newline=None, closefd=True, opener=None):
if newline is not None:
warnings.warn('newline is not supported in py2')
if not closefd:
warnings.warn('closefd is not supported in py2')
if opener is not None:
warnings.warn('opener is not supported in py2')
return codecs.open(filename=file, mode=mode, encoding=encoding,
errors=errors, buffering=buffering)
from time import time
from collections import OrderedDict
from importlib import import_module
@ -282,7 +303,6 @@ class ExtraArgumentParser(object):
if iface in klass.skipped_iface:
continue
self.extra[klass.name] = klass
logger.debug("extra parameter classes loaded: %s", self.extra.keys())
def validate(self, arg_name, parameters):
"""
@ -294,7 +314,7 @@ class ExtraArgumentParser(object):
"""
# Iterate over parameters to validate
for p, v in parameters.items():
for p in list(parameters):
klass = self.extra.get(p, None)
if not klass:
# Remove unknown parameters
@ -302,7 +322,7 @@ class ExtraArgumentParser(object):
else:
try:
# Validate parameter value
parameters[p] = klass.validate(v, arg_name)
parameters[p] = klass.validate(parameters[p], arg_name)
except Exception as e:
logger.error(
"unable to validate extra parameter '%s' "
@ -398,36 +418,32 @@ class ActionsMap(object):
Moreover, the action can have specific argument(s).
This class allows to manipulate one or several actions maps
associated to a namespace. If no namespace is given, it will load
all available namespaces.
associated to a namespace.
Keyword arguments:
- parser_class -- The BaseActionsMapParser derived class to use
for parsing the actions map
- namespaces -- The list of namespaces to use
- use_cache -- False if it should parse the actions map file
instead of using the cached one
- parser_kwargs -- A dict of arguments to pass to the parser
class at construction
- top_parser -- A BaseActionsMapParser-derived instance to use for
parsing the actions map
- load_only_category -- A name of a category that should only be the
one loaded because it's been already determined
that's the only one relevant ... used for optimization
purposes...
"""
def __init__(self, parser_class, namespaces=[], use_cache=True, parser_kwargs={}):
if not issubclass(parser_class, BaseActionsMapParser):
raise ValueError("Invalid parser class '%s'" % parser_class.__name__)
self.parser_class = parser_class
self.use_cache = use_cache
def __init__(self, top_parser, load_only_category=None):
assert isinstance(top_parser, BaseActionsMapParser), (
"Invalid parser class '%s'" % top_parser.__class__.__name__
)
moulinette_env = init_moulinette_env()
DATA_DIR = moulinette_env["DATA_DIR"]
CACHE_DIR = moulinette_env["CACHE_DIR"]
if len(namespaces) == 0:
namespaces = self.get_namespaces()
actionsmaps = OrderedDict()
self.from_cache = False
# Iterate over actions map namespaces
for n in namespaces:
for n in self.get_namespaces():
logger.debug("loading actions map namespace '%s'", n)
actionsmap_yml = "%s/actionsmap/%s.yml" % (DATA_DIR, n)
@ -439,33 +455,36 @@ class ActionsMap(object):
actionsmap_yml_stat.st_mtime,
)
if use_cache and os.path.exists(actionsmap_pkl):
if os.path.exists(actionsmap_pkl):
try:
# Attempt to load cache
with open(actionsmap_pkl, "rb") as f:
actionsmaps[n] = pickle.load(f)
self.from_cache = True
# TODO: Switch to python3 and catch proper exception
except (IOError, EOFError):
self.use_cache = False
actionsmaps = self.generate_cache(namespaces)
elif use_cache: # cached file doesn't exists
self.use_cache = False
actionsmaps = self.generate_cache(namespaces)
elif n not in actionsmaps:
with open(actionsmap_yml) as f:
actionsmaps[n] = ordered_yaml_load(f)
actionsmaps[n] = self.generate_cache(n)
else: # cache file doesn't exists
actionsmaps[n] = self.generate_cache(n)
# If load_only_category is set, and *if* the target category
# is in the actionsmap, we'll load only that one.
# If we filter it even if it doesn't exist, we'll end up with a
# weird help message when we do a typo in the category name..
if load_only_category and load_only_category in actionsmaps[n]:
actionsmaps[n] = {
k: v
for k, v in actionsmaps[n].items()
if k in [load_only_category, "_global"]
}
# Load translations
m18n.load_namespace(n)
# Generate parsers
self.extraparser = ExtraArgumentParser(parser_class.interface)
self._parser = self._construct_parser(actionsmaps, **parser_kwargs)
@property
def parser(self):
"""Return the instance of the interface's actions map parser"""
return self._parser
self.extraparser = ExtraArgumentParser(top_parser.interface)
self.parser = self._construct_parser(actionsmaps, top_parser)
def get_authenticator_for_profile(self, auth_profile):
@ -563,6 +582,9 @@ class ActionsMap(object):
)
func = getattr(mod, func_name)
except (AttributeError, ImportError):
import traceback
traceback.print_exc()
logger.exception("unable to load function %s.%s", namespace, func_name)
raise MoulinetteError("error_see_log")
else:
@ -601,85 +623,81 @@ class ActionsMap(object):
moulinette_env = init_moulinette_env()
DATA_DIR = moulinette_env["DATA_DIR"]
for f in os.listdir("%s/actionsmap" % DATA_DIR):
if f.endswith(".yml"):
namespaces.append(f[:-4])
# This var is ['*'] by default but could be set for example to
# ['yunohost', 'yml_*']
NAMESPACE_PATTERNS = moulinette_env["NAMESPACES"]
# Look for all files that match the given patterns in the actionsmap dir
for namespace_pattern in NAMESPACE_PATTERNS:
namespaces.extend(
glob.glob("%s/actionsmap/%s.yml" % (DATA_DIR, namespace_pattern))
)
# Keep only the filenames with extension
namespaces = [os.path.basename(n)[:-4] for n in namespaces]
return namespaces
@classmethod
def generate_cache(klass, namespaces=None):
def generate_cache(klass, namespace):
"""
Generate cache for the actions map's file(s)
Keyword arguments:
- namespaces -- A list of namespaces to generate cache for
- namespace -- The namespace to generate cache for
Returns:
A dict of actions map for each namespaces
The action map for the namespace
"""
moulinette_env = init_moulinette_env()
CACHE_DIR = moulinette_env["CACHE_DIR"]
DATA_DIR = moulinette_env["DATA_DIR"]
actionsmaps = {}
if not namespaces:
namespaces = klass.get_namespaces()
# Iterate over actions map namespaces
for n in namespaces:
logger.debug("generating cache for actions map namespace '%s'", n)
logger.debug("generating cache for actions map namespace '%s'", namespace)
# Read actions map from yaml file
am_file = "%s/actionsmap/%s.yml" % (DATA_DIR, n)
with open(am_file, "r") as f:
actionsmaps[n] = ordered_yaml_load(f)
# Read actions map from yaml file
am_file = "%s/actionsmap/%s.yml" % (DATA_DIR, namespace)
with open(am_file, "r") as f:
actionsmap = ordered_yaml_load(f)
# at installation, cachedir might not exists
if os.path.exists("%s/actionsmap/" % CACHE_DIR):
# clean old cached files
for i in os.listdir("%s/actionsmap/" % CACHE_DIR):
if i.endswith(".pkl"):
os.remove("%s/actionsmap/%s" % (CACHE_DIR, i))
# at installation, cachedir might not exists
for old_cache in glob.glob("%s/actionsmap/%s-*.pkl" % (CACHE_DIR, namespace)):
os.remove(old_cache)
# Cache actions map into pickle file
am_file_stat = os.stat(am_file)
# Cache actions map into pickle file
am_file_stat = os.stat(am_file)
pkl = "%s-%d-%d.pkl" % (n, am_file_stat.st_size, am_file_stat.st_mtime)
pkl = "%s-%d-%d.pkl" % (namespace, am_file_stat.st_size, am_file_stat.st_mtime)
with open_cachefile(pkl, "wb", subdir="actionsmap") as f:
pickle.dump(actionsmaps[n], f)
with open_cachefile(pkl, "wb", subdir="actionsmap") as f:
pickle.dump(actionsmap, f)
return actionsmaps
return actionsmap
# Private methods
def _construct_parser(self, actionsmaps, **kwargs):
def _construct_parser(self, actionsmaps, top_parser):
"""
Construct the parser with the actions map
Keyword arguments:
- actionsmaps -- A dict of multi-level dictionnary of
categories/actions/arguments list for each namespaces
- **kwargs -- Additionnal arguments to pass at the parser
class instantiation
- top_parser -- A BaseActionsMapParser-derived instance to use for
parsing the actions map
Returns:
An interface relevant's parser object
"""
# Get extra parameters
if self.use_cache:
validate_extra = False
else:
validate_extra = True
# Instantiate parser
#
# this either returns:
# * moulinette.interfaces.cli.ActionsMapParser
# * moulinette.interfaces.api.ActionsMapParser
top_parser = self.parser_class(**kwargs)
logger.debug("building parser...")
start = time()
# If loading from cache, extra were already checked when cache was
# loaded ? Not sure about this ... old code is a bit mysterious...
validate_extra = not self.from_cache
# namespace, actionsmap is a tuple where:
#
@ -781,4 +799,5 @@ class ActionsMap(object):
tid, action_options["configuration"]
)
logger.debug("building parser took %.3fs", time() - start)
return top_parser

View file

@ -5,7 +5,7 @@ import logging
import hashlib
import hmac
from moulinette.cache import open_cachefile, get_cachedir
from moulinette.cache import open_cachefile, get_cachedir, cachefile_exists
from moulinette.core import MoulinetteError
logger = logging.getLogger("moulinette.authenticator")
@ -159,6 +159,10 @@ class BaseAuthenticator(object):
"%s.asc" % session_id, mode, subdir="session/%s" % self.name
)
def _session_exists(self, session_id):
"""Check a session exists"""
return cachefile_exists("%s.asc" % session_id, subdir="session/%s" % self.name)
def _store_session(self, session_id, session_token):
"""Store a session to be able to use it later to reauthenticate"""
@ -170,6 +174,8 @@ class BaseAuthenticator(object):
def _authenticate_session(self, session_id, session_token):
"""Checks session and token against the stored session token"""
if not self._session_exists(session_id):
raise MoulinetteError("session_expired")
try:
# FIXME : shouldn't we also add a check that this session file
# is not too old ? e.g. not older than 24 hours ? idk...

View file

@ -2,15 +2,15 @@
# TODO: Use Python3 to remove this fix!
from __future__ import absolute_import
import os
import logging
import random
import string
import crypt
import ldap
import ldap.sasl
import time
import ldap.modlist as modlist
from moulinette.core import MoulinetteError
from moulinette import m18n
from moulinette.core import MoulinetteError, MoulinetteLdapIsDownError
from moulinette.authenticators import BaseAuthenticator
logger = logging.getLogger("moulinette.authenticator.ldap")
@ -69,7 +69,7 @@ class Authenticator(BaseAuthenticator):
# Implement virtual methods
def authenticate(self, password=None):
try:
def _reconnect():
con = ldap.ldapobject.ReconnectLDAPObject(
self._get_uri(), retry_max=10, retry_delay=0.5
)
@ -80,11 +80,23 @@ class Authenticator(BaseAuthenticator):
con.simple_bind_s(self.userdn, password)
else:
con.simple_bind_s()
return con
try:
con = _reconnect()
except ldap.INVALID_CREDENTIALS:
raise MoulinetteError("invalid_password")
except ldap.SERVER_DOWN:
logger.exception("unable to reach the server to authenticate")
raise MoulinetteError("ldap_server_down")
# ldap is down, attempt to restart it before really failing
logger.warning(m18n.g("ldap_server_is_down_restart_it"))
os.system("systemctl restart slapd")
time.sleep(10) # waits 10 secondes so we are sure that slapd has restarted
try:
con = _reconnect()
except ldap.SERVER_DOWN:
raise MoulinetteLdapIsDownError("ldap_server_down")
# Check that we are indeed logged in with the right identity
try:
@ -99,30 +111,6 @@ class Authenticator(BaseAuthenticator):
raise MoulinetteError("Not logged in with the expected userdn ?!")
else:
self.con = con
self._ensure_password_uses_strong_hash(password)
def _ensure_password_uses_strong_hash(self, password):
# XXX this has been copy pasted from YunoHost, should we put that into moulinette?
def _hash_user_password(password):
char_set = (
string.ascii_uppercase + string.ascii_lowercase + string.digits + "./"
)
salt = "".join([random.SystemRandom().choice(char_set) for x in range(16)])
salt = "$6$" + salt + "$"
return "{CRYPT}" + crypt.crypt(str(password), salt)
hashed_password = self.search(self.admindn, attrs=["userPassword"])[0]
# post-install situation, password is not already set
if "userPassword" not in hashed_password or not hashed_password["userPassword"]:
return
# we aren't using sha-512 but something else that is weaker, proceed to upgrade
if not hashed_password["userPassword"][0].startswith("{CRYPT}$6$"):
self.update(
"cn=%s" % self.adminuser,
{"userPassword": [_hash_user_password(password)]},
)
# Additional LDAP methods
# TODO: Review these methods
@ -148,15 +136,11 @@ class Authenticator(BaseAuthenticator):
try:
result = self.con.search_s(base, ldap.SCOPE_SUBTREE, filter, attrs)
except Exception as e:
logger.exception(
raise MoulinetteError(
"error during LDAP search operation with: base='%s', "
"filter='%s', attrs=%s and exception %s",
base,
filter,
attrs,
e,
"filter='%s', attrs=%s and exception %s" % (base, filter, attrs, e),
raw_msg=True,
)
raise MoulinetteError("ldap_operation_error", action="search")
result_list = []
if not attrs or "dn" not in attrs:
@ -185,14 +169,11 @@ class Authenticator(BaseAuthenticator):
try:
self.con.add_s(dn, ldif)
except Exception as e:
logger.exception(
raise MoulinetteError(
"error during LDAP add operation with: rdn='%s', "
"attr_dict=%s and exception %s",
rdn,
attr_dict,
e,
"attr_dict=%s and exception %s" % (rdn, attr_dict, e),
raw_msg=True,
)
raise MoulinetteError("ldap_operation_error", action="add")
else:
return True
@ -211,12 +192,11 @@ class Authenticator(BaseAuthenticator):
try:
self.con.delete_s(dn)
except Exception as e:
logger.exception(
"error during LDAP delete operation with: rdn='%s' and exception %s",
rdn,
e,
raise MoulinetteError(
"error during LDAP delete operation with: rdn='%s' and exception %s"
% (rdn, e),
raw_msg=True,
)
raise MoulinetteError("ldap_operation_error", action="remove")
else:
return True
@ -249,15 +229,12 @@ class Authenticator(BaseAuthenticator):
self.con.modify_ext_s(dn, ldif)
except Exception as e:
logger.exception(
raise MoulinetteError(
"error during LDAP update operation with: rdn='%s', "
"attr_dict=%s, new_rdn=%s and exception: %s",
rdn,
attr_dict,
new_rdn,
e,
"attr_dict=%s, new_rdn=%s and exception: %s"
% (rdn, attr_dict, new_rdn, e),
raw_msg=True,
)
raise MoulinetteError("ldap_operation_error", action="update")
else:
return True

View file

@ -42,3 +42,10 @@ def open_cachefile(filename, mode="r", subdir=""):
cache_dir = get_cachedir(subdir, make_dir=True if mode[0] == "w" else False)
file_path = os.path.join(cache_dir, filename)
return open(file_path, mode)
def cachefile_exists(filename, subdir=""):
cache_dir = get_cachedir(subdir, make_dir=False)
file_path = os.path.join(cache_dir, filename)
return os.path.exists(file_path)

View file

@ -5,8 +5,6 @@ import time
import json
import logging
from importlib import import_module
import moulinette
from moulinette.globals import init_moulinette_env
@ -372,53 +370,6 @@ class MoulinetteSignals(object):
raise NotImplementedError("this signal is not handled")
# Interfaces & Authenticators management -------------------------------
def init_interface(name, kwargs={}, actionsmap={}):
"""Return a new interface instance
Retrieve the given interface module and return a new instance of its
Interface class. It is initialized with arguments 'kwargs' and
connected to 'actionsmap' if it's an ActionsMap object, otherwise
a new ActionsMap instance will be initialized with arguments
'actionsmap'.
Keyword arguments:
- name -- The interface name
- kwargs -- A dict of arguments to pass to Interface
- actionsmap -- Either an ActionsMap instance or a dict of
arguments to pass to ActionsMap
"""
from moulinette.actionsmap import ActionsMap
try:
mod = import_module("moulinette.interfaces.%s" % name)
except ImportError as e:
logger.exception("unable to load interface '%s' : %s", name, e)
raise MoulinetteError("error_see_log")
else:
try:
# Retrieve interface classes
parser = mod.ActionsMapParser
interface = mod.Interface
except AttributeError:
logger.exception("unable to retrieve classes of interface '%s'", name)
raise MoulinetteError("error_see_log")
# Instantiate or retrieve ActionsMap
if isinstance(actionsmap, dict):
amap = ActionsMap(actionsmap.pop("parser", parser), **actionsmap)
elif isinstance(actionsmap, ActionsMap):
amap = actionsmap
else:
logger.error("invalid actionsmap value %r", actionsmap)
raise MoulinetteError("error_see_log")
return interface(amap, **kwargs)
# Moulinette core classes ----------------------------------------------
@ -435,6 +386,10 @@ class MoulinetteError(Exception):
self.strerror = msg
class MoulinetteLdapIsDownError(MoulinetteError):
"""Used when ldap is down"""
class MoulinetteLock(object):
"""Locker for a moulinette instance

View file

@ -11,4 +11,7 @@ def init_moulinette_env():
"MOULINETTE_LOCALES_DIR", "/usr/share/moulinette/locale"
),
"CACHE_DIR": environ.get("MOULINETTE_CACHE_DIR", "/var/cache/moulinette"),
"NAMESPACES": environ.get(
"MOULINETTE_NAMESPACES", "*"
).split(), # By default we'll load every namespace we find
}

View file

@ -342,11 +342,6 @@ class _CallbackAction(argparse.Action):
self.callback_method = callback.get("method")
self.callback_kwargs = callback.get("kwargs", {})
self.callback_return = callback.get("return", False)
logger.debug(
"registering new callback action '{0}' to {1}".format(
self.callback_method, option_strings
)
)
@property
def callback(self):
@ -361,6 +356,9 @@ class _CallbackAction(argparse.Action):
mod = __import__(mod_name, globals=globals(), level=0, fromlist=[func_name])
func = getattr(mod, func_name)
except (AttributeError, ImportError):
import traceback
traceback.print_exc()
raise ValueError("unable to import method {0}".format(self.callback_method))
self._callback = func

View file

@ -10,10 +10,11 @@ from gevent import sleep
from gevent.queue import Queue
from geventwebsocket import WebSocketError
from bottle import run, request, response, Bottle, HTTPResponse
from bottle import request, response, Bottle, HTTPResponse
from bottle import abort
from moulinette import msignals, m18n, env
from moulinette.actionsmap import ActionsMap
from moulinette.core import MoulinetteError
from moulinette.interfaces import (
BaseActionsMapParser,
@ -219,22 +220,18 @@ class _ActionsMapPlugin(object):
Keyword arguments:
- actionsmap -- An ActionsMap instance
- use_websocket -- If true, install a WebSocket on /messages in order
to serve messages coming from the 'display' signal
"""
name = "actionsmap"
api = 2
def __init__(self, actionsmap, use_websocket, log_queues={}):
def __init__(self, actionsmap, log_queues={}):
# Connect signals to handlers
msignals.set_handler("authenticate", self._do_authenticate)
if use_websocket:
msignals.set_handler("display", self._do_display)
msignals.set_handler("display", self._do_display)
self.actionsmap = actionsmap
self.use_websocket = use_websocket
self.log_queues = log_queues
# TODO: Save and load secrets?
self.secrets = {}
@ -290,13 +287,9 @@ class _ActionsMapPlugin(object):
)
# Append messages route
if self.use_websocket:
app.route(
"/messages",
name="messages",
callback=self.messages,
skip=["actionsmap"],
)
app.route(
"/messages", name="messages", callback=self.messages, skip=["actionsmap"],
)
# Append routes from the actions map
for (m, p) in self.actionsmap.parser.routes:
@ -327,11 +320,10 @@ class _ActionsMapPlugin(object):
# Append other request params
for k, v in request.params.dict.items():
v = _format(v)
try:
curr_v = params[k]
except KeyError:
if k not in params.keys():
params[k] = v
else:
curr_v = params[k]
# Append param value to the list
if not isinstance(curr_v, list):
curr_v = [curr_v]
@ -362,13 +354,24 @@ class _ActionsMapPlugin(object):
"""
# Retrieve session values
s_id = request.get_cookie("session.id") or random_ascii()
try:
s_id = request.get_cookie("session.id") or random_ascii()
except:
# Super rare case where there are super weird cookie / cache issue
# Previous line throws a CookieError that creates a 500 error ...
# So let's catch it and just use a fresh ID then...
s_id = random_ascii()
try:
s_secret = self.secrets[s_id]
except KeyError:
s_tokens = {}
else:
s_tokens = request.get_cookie("session.tokens", secret=s_secret) or {}
try:
s_tokens = request.get_cookie("session.tokens", secret=s_secret) or {}
except:
# Same as for session.id a few lines before
s_tokens = {}
s_new_token = random_ascii()
try:
@ -574,6 +577,9 @@ def format_for_response(content):
return ""
response.status = 200
if isinstance(content, HTTPResponse):
return content
# Return JSON-style response
response.content_type = "application/json"
return json_encode(content, cls=JSONExtendedEncoder)
@ -735,17 +741,16 @@ class Interface(BaseInterface):
actions map.
Keyword arguments:
- actionsmap -- The ActionsMap instance to connect to
- routes -- A dict of additional routes to add in the form of
{(method, path): callback}
- use_websocket -- Serve via WSGI to handle asynchronous responses
- log_queues -- A LogQueues object or None to retrieve it from
registered logging handlers
"""
def __init__(self, actionsmap, routes={}, use_websocket=True, log_queues=None):
self.use_websocket = use_websocket
def __init__(self, routes={}, log_queues=None):
actionsmap = ActionsMap(ActionsMapParser())
# Attempt to retrieve log queues from an APIQueueHandler
if log_queues is None:
@ -766,18 +771,21 @@ class Interface(BaseInterface):
# Attempt to retrieve and set locale
def api18n(callback):
try:
locale = request.params.pop("locale")
except KeyError:
locale = m18n.default_locale
m18n.set_locale(locale)
return callback
def wrapper(*args, **kwargs):
try:
locale = request.params.pop("locale")
except KeyError:
locale = m18n.default_locale
m18n.set_locale(locale)
return callback(*args, **kwargs)
return wrapper
# Install plugins
app.install(filter_csrf)
app.install(apiheader)
app.install(api18n)
app.install(_ActionsMapPlugin(actionsmap, use_websocket, log_queues))
app.install(_ActionsMapPlugin(actionsmap, log_queues))
# Append default routes
# app.route(['/api', '/api/<category:re:[a-z]+>'], method='GET',
@ -802,23 +810,15 @@ class Interface(BaseInterface):
"""
logger.debug(
"starting the server instance in %s:%d with websocket=%s",
host,
port,
self.use_websocket,
"starting the server instance in %s:%d", host, port,
)
try:
if self.use_websocket:
from gevent.pywsgi import WSGIServer
from geventwebsocket.handler import WebSocketHandler
from gevent.pywsgi import WSGIServer
from geventwebsocket.handler import WebSocketHandler
server = WSGIServer(
(host, port), self._app, handler_class=WebSocketHandler
)
server.serve_forever()
else:
run(self._app, host=host, port=port)
server = WSGIServer((host, port), self._app, handler_class=WebSocketHandler)
server.serve_forever()
except IOError as e:
logger.exception("unable to start the server instance on %s:%d", host, port)
if e.args[0] == errno.EADDRINUSE:

View file

@ -7,12 +7,12 @@ import locale
import logging
from argparse import SUPPRESS
from collections import OrderedDict
import pytz
from datetime import date, datetime
import argcomplete
from moulinette import msignals, m18n
from moulinette.actionsmap import ActionsMap
from moulinette.core import MoulinetteError
from moulinette.interfaces import (
BaseActionsMapParser,
@ -103,6 +103,8 @@ def pretty_date(_date):
Argument:
- date -- The date or datetime to display
"""
import pytz # Lazy loading, this takes like 3+ sec on a RPi2 ?!
# Deduce system timezone
nowutc = datetime.now(tz=pytz.utc)
nowtz = datetime.now()
@ -168,8 +170,13 @@ def pretty_print_dict(d, depth=0):
def get_locale():
"""Return current user locale"""
lang = locale.getdefaultlocale()[0]
"""Return current user eocale"""
try:
lang = locale.getdefaultlocale()[0]
except Exception:
# In some edge case the locale lib fails ...
# c.f. https://forum.yunohost.org/t/error-when-trying-to-enter-user-information-in-admin-panel/11390/11
lang = os.getenv("LANG")
if not lang:
return ""
return lang[:2]
@ -418,7 +425,8 @@ class Interface(BaseInterface):
"""
def __init__(self, actionsmap):
def __init__(self, top_parser=None, load_only_category=None):
# Set user locale
m18n.set_locale(get_locale())
@ -428,9 +436,12 @@ class Interface(BaseInterface):
msignals.set_handler("authenticate", self._do_authenticate)
msignals.set_handler("prompt", self._do_prompt)
self.actionsmap = actionsmap
self.actionsmap = ActionsMap(
ActionsMapParser(top_parser=top_parser),
load_only_category=load_only_category,
)
def run(self, args, output_as=None, password=None, timeout=None):
def run(self, args, output_as=None, timeout=None):
"""Run the moulinette
Process the action corresponding to the given arguments 'args'
@ -442,7 +453,6 @@ class Interface(BaseInterface):
- json: return a JSON encoded string
- plain: return a script-readable output
- none: do not output the result
- password -- The password to use in case of authentication
- timeout -- Number of seconds before this command will timeout because it can't acquire the lock (meaning that another command is currently running), by default there is no timeout and the command will wait until it can get the lock
"""
@ -453,11 +463,7 @@ class Interface(BaseInterface):
argcomplete.autocomplete(self.actionsmap.parser._parser)
# Set handler for authentication
if password:
msignals.set_handler("authenticate", lambda a: a(password=password))
else:
if os.isatty(1):
msignals.set_handler("authenticate", self._do_authenticate)
msignals.set_handler("authenticate", self._do_authenticate)
try:
ret = self.actionsmap.process(args, timeout=timeout)
@ -489,7 +495,11 @@ class Interface(BaseInterface):
Handle the core.MoulinetteSignals.authenticate signal.
"""
# TODO: Allow token authentication?
# Hmpf we have no-use case in yunohost anymore where we need to auth
# because everything is run as root ...
# I guess we could imagine some yunohost-independant use-case where
# moulinette is used to create a CLI for non-root user that needs to
# auth somehow but hmpf -.-
help = authenticator.extra.get("help")
msg = m18n.n(help) if help else m18n.g("password")
return authenticator(password=self._do_prompt(msg, True, False, color="yellow"))

View file

@ -65,6 +65,7 @@ def configure_logging(logging_config=None):
# load configuration from dict
dictConfig(DEFAULT_LOGGING)
if logging_config:
logging.main_logger = logging_config.get("main_logger")
dictConfig(logging_config)

View file

@ -28,7 +28,7 @@ def check_output(args, stderr=subprocess.STDOUT, shell=True, **kwargs):
and use shell by default before calling subprocess.check_output.
"""
return subprocess.check_output(args, stderr=stderr, shell=shell, **kwargs)
return subprocess.check_output(args, stderr=stderr, shell=shell, **kwargs).strip()
# Call with stream access ----------------------------------------------

View file

@ -1,7 +1,6 @@
import logging
from json.encoder import JSONEncoder
import datetime
import pytz
logger = logging.getLogger("moulinette.utils.serialize")
@ -23,6 +22,9 @@ class JSONExtendedEncoder(JSONEncoder):
"""
def default(self, o):
import pytz # Lazy loading, this takes like 3+ sec on a RPi2 ?!
"""Return a serializable object"""
# Convert compatible containers into list
if isinstance(o, set) or (hasattr(o, "__iter__") and hasattr(o, "next")):

View file

@ -17,6 +17,31 @@ if "install" in sys.argv:
if f.endswith('.json'):
locale_files.append('locales/%s' % f)
install_deps = [
'argcomplete',
'psutil',
'pytz',
'pyyaml',
'toml',
'python-ldap',
'gevent-websocket',
'bottle',
]
test_deps = [
'pytest',
'pytest-cov',
'pytest-env',
'pytest-mock',
'requests',
'requests-mock',
'webtest'
]
extras = {
'install': install_deps,
'tests': test_deps,
}
setup(name='Moulinette',
version='2.0.0',
@ -27,24 +52,8 @@ setup(name='Moulinette',
license='AGPL',
packages=find_packages(exclude=['test']),
data_files=[(LOCALES_DIR, locale_files)],
python_requires='>=3.5',
install_requires=[
'argcomplete',
'psutil',
'pytz',
'pyyaml',
'toml',
'python-ldap',
'gevent-websocket',
'bottle',
],
tests_require=[
'pytest',
'pytest-cov',
'pytest-env',
'pytest-mock',
'requests',
'requests-mock',
'webtest'
],
python_requires='>=2.7.*, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4',
install_requires=install_deps,
tests_require=test_deps,
extras_require=extras,
)

View file

@ -7,7 +7,7 @@ import os
import shutil
import pytest
from src.ldap_server import LDAPServer
from .src.ldap_server import LDAPServer
def patch_init(moulinette):
@ -125,13 +125,9 @@ def moulinette_webapi(moulinette):
CookiePolicy.return_ok_secure = return_true
moulinette_webapi = moulinette.core.init_interface(
"api",
kwargs={"routes": {}, "use_websocket": False},
actionsmap={"namespaces": ["moulitest"], "use_cache": True},
)
from moulinette.interfaces.api import Interface as Api
return TestApp(moulinette_webapi._app)
return TestApp(Api(routes={})._app)
@pytest.fixture
@ -148,17 +144,12 @@ def moulinette_cli(moulinette, mocker):
help="Log and print debug messages",
)
mocker.patch("os.isatty", return_value=True)
moulinette_cli = moulinette.core.init_interface(
"cli",
actionsmap={
"namespaces": ["moulitest"],
"use_cache": False,
"parser_kwargs": {"top_parser": parser},
},
)
from moulinette.interfaces.cli import Interface as Cli
cli = Cli(top_parser=parser)
mocker.stopall()
return moulinette_cli
return cli
@pytest.fixture

View file

@ -98,7 +98,23 @@ class LDAPServer:
"posixAccount",
"simpleSecurityObject",
],
"userPassword": ["yunohost"],
"userPassword": [self._hash_user_password("yunohost")],
}
ldap_interface.update("cn=admin", admin_dict)
def _hash_user_password(self, password):
"""
Copy pasta of what's in yunohost/user.py
"""
import string
import random
import crypt
char_set = (
string.ascii_uppercase + string.ascii_lowercase + string.digits + "./"
)
salt = "".join([random.SystemRandom().choice(char_set) for x in range(16)])
salt = "$6$" + salt + "$"
return "{CRYPT}" + crypt.crypt(str(password), salt)

View file

@ -158,10 +158,10 @@ def test_required_paremeter_missing_value(iface, caplog):
def test_actions_map_unknown_authenticator(monkeypatch, tmp_path):
monkeypatch.setenv("MOULINETTE_DATA_DIR", str(tmp_path))
actionsmap_dir = actionsmap_dir = tmp_path / "actionsmap"
actionsmap_dir = tmp_path / "actionsmap"
actionsmap_dir.mkdir()
amap = ActionsMap(BaseActionsMapParser)
amap = ActionsMap(BaseActionsMapParser())
with pytest.raises(ValueError) as exception:
amap.get_authenticator_for_profile("unknown")
assert "Unknown authenticator" in str(exception)
@ -225,7 +225,7 @@ def test_extra_argument_parser_parse_args(iface, mocker):
def test_actions_map_api():
from moulinette.interfaces.api import ActionsMapParser
amap = ActionsMap(ActionsMapParser, use_cache=False)
amap = ActionsMap(ActionsMapParser())
assert amap.parser.global_conf["authenticate"] == "all"
assert "default" in amap.parser.global_conf["authenticator"]
@ -233,9 +233,9 @@ def test_actions_map_api():
assert ("GET", "/test-auth/default") in amap.parser.routes
assert ("POST", "/test-auth/subcat/post") in amap.parser.routes
amap.generate_cache()
amap.generate_cache("moulitest")
amap = ActionsMap(ActionsMapParser, use_cache=True)
amap = ActionsMap(ActionsMapParser())
assert amap.parser.global_conf["authenticate"] == "all"
assert "default" in amap.parser.global_conf["authenticator"]
@ -247,17 +247,24 @@ def test_actions_map_api():
def test_actions_map_import_error(mocker):
from moulinette.interfaces.api import ActionsMapParser
amap = ActionsMap(ActionsMapParser)
amap = ActionsMap(ActionsMapParser())
from moulinette.core import MoulinetteLock
mocker.patch.object(MoulinetteLock, "_is_son_of", return_value=False)
mocker.patch("__builtin__.__import__", side_effect=ImportError)
orig_import = __import__
def import_mock(name, globals={}, locals={}, fromlist=[], level=-1):
if name == "moulitest.testauth":
mocker.stopall()
raise ImportError
return orig_import(name, globals, locals, fromlist, level)
mocker.patch("__builtin__.__import__", side_effect=import_mock)
with pytest.raises(MoulinetteError) as exception:
amap.process({}, timeout=30, route=("GET", "/test-auth/none"))
mocker.stopall()
translation = m18n.g("error_see_log")
expected_msg = translation.format()
assert expected_msg in str(exception)
@ -274,9 +281,7 @@ def test_actions_map_cli():
default=False,
help="Log and print debug messages",
)
amap = ActionsMap(
ActionsMapParser, use_cache=False, parser_kwargs={"top_parser": parser}
)
amap = ActionsMap(ActionsMapParser(top_parser=parser))
assert amap.parser.global_conf["authenticate"] == "all"
assert "default" in amap.parser.global_conf["authenticator"]
@ -293,11 +298,9 @@ def test_actions_map_cli():
.choices
)
amap.generate_cache()
amap.generate_cache("moulitest")
amap = ActionsMap(
ActionsMapParser, use_cache=True, parser_kwargs={"top_parser": parser}
)
amap = ActionsMap(ActionsMapParser(top_parser=parser))
assert amap.parser.global_conf["authenticate"] == "all"
assert "default" in amap.parser.global_conf["authenticator"]

View file

@ -216,18 +216,15 @@ class TestAuthCLI:
assert "some_data_from_default" in message.out
moulinette_cli.run(
["testauth", "default"], output_as="plain", password="default"
)
moulinette_cli.run(["testauth", "default"], output_as="plain")
message = capsys.readouterr()
assert "some_data_from_default" in message.out
def test_login_bad_password(self, moulinette_cli, capsys, mocker):
mocker.patch("getpass.getpass", return_value="Bad Password")
with pytest.raises(MoulinetteError):
moulinette_cli.run(
["testauth", "default"], output_as="plain", password="Bad Password"
)
moulinette_cli.run(["testauth", "default"], output_as="plain")
mocker.patch("getpass.getpass", return_value="Bad Password")
with pytest.raises(MoulinetteError):
@ -242,10 +239,9 @@ class TestAuthCLI:
expected_msg = translation.format()
assert expected_msg in str(exception)
mocker.patch("getpass.getpass", return_value="yoloswag")
with pytest.raises(MoulinetteError) as exception:
moulinette_cli.run(
["testauth", "default"], output_as="none", password="yoloswag"
)
moulinette_cli.run(["testauth", "default"], output_as="none")
expected_msg = translation.format()
assert expected_msg in str(exception)

View file

@ -371,6 +371,11 @@ def test_mkdir(tmp_path):
def test_mkdir_with_permission(tmp_path, mocker):
# This test only make sense when not being root
if os.getuid() == 0:
return
new_path = tmp_path / "new_folder"
permission = 0o700
mkdir(str(new_path), mode=permission)

View file

@ -66,11 +66,14 @@ class TestLDAP:
assert ldap_interface.con
def test_authenticate_server_down(self, ldap_server):
def test_authenticate_server_down(self, ldap_server, mocker):
self.ldap_conf["parameters"]["uri"] = ldap_server.uri
self.ldap_conf["parameters"]["user_rdn"] = "cn=admin,dc=yunohost,dc=org"
ldap_server.stop()
ldap_interface = m_ldap.Authenticator(**self.ldap_conf)
# Now if slapd is down, moulinette tries to restart it
mocker.patch("os.system")
with pytest.raises(MoulinetteError) as exception:
ldap_interface.authenticate(password="yunohost")
@ -208,9 +211,10 @@ class TestLDAP:
with pytest.raises(MoulinetteError) as exception:
self.add_new_user(ldap_interface)
translation = m18n.g("ldap_operation_error", action="add")
expected_msg = translation.format(action="add")
assert expected_msg in str(exception)
expected_message = "error during LDAP add operation with: rdn="
expected_error = "modifications require authentication"
assert expected_error in str(exception)
assert expected_message in str(exception)
def remove_new_user(self, ldap_interface):
new_user_info = self.add_new_user(
@ -229,9 +233,10 @@ class TestLDAP:
"uid=%s,ou=users,dc=yunohost,dc=org" % uid, attrs=None
)
translation = m18n.g("ldap_operation_error", action="search")
expected_msg = translation.format(action="search")
assert expected_msg in str(exception)
expected_message = "error during LDAP search operation with: base="
expected_error = "No such object"
assert expected_error in str(exception)
assert expected_message in str(exception)
def test_admin_remove(self, ldap_server):
self.ldap_conf["parameters"]["uri"] = ldap_server.uri
@ -257,9 +262,10 @@ class TestLDAP:
with pytest.raises(MoulinetteError) as exception:
self.remove_new_user(ldap_interface)
translation = m18n.g("ldap_operation_error", action="remove")
expected_msg = translation.format(action="remove")
assert expected_msg in str(exception)
expected_message = "error during LDAP delete operation with: rdn="
expected_error = "modifications require authentication"
assert expected_error in str(exception)
assert expected_message in str(exception)
def update_new_user(self, ldap_interface, new_rdn=False):
new_user_info = self.add_new_user(
@ -336,9 +342,10 @@ class TestLDAP:
with pytest.raises(MoulinetteError) as exception:
self.update_new_user(ldap_interface)
translation = m18n.g("ldap_operation_error", action="update")
expected_msg = translation.format(action="update")
assert expected_msg in str(exception)
expected_message = "error during LDAP update operation with: rdn="
expected_error = "modifications require authentication"
assert expected_error in str(exception)
assert expected_message in str(exception)
def test_anonymous_update_new_rdn(self, ldap_server):
self.ldap_conf["parameters"]["uri"] = ldap_server.uri
@ -347,9 +354,10 @@ class TestLDAP:
with pytest.raises(MoulinetteError) as exception:
self.update_new_user(ldap_interface, True)
translation = m18n.g("ldap_operation_error", action="update")
expected_msg = translation.format(action="update")
assert expected_msg in str(exception)
expected_message = "error during LDAP update operation with: rdn="
expected_error = "modifications require authentication"
assert expected_error in str(exception)
assert expected_message in str(exception)
def test_empty_update(self, ldap_server):
self.ldap_conf["parameters"]["uri"] = ldap_server.uri

View file

@ -115,6 +115,6 @@ def test_call_async_output_kwargs(test_file, mocker):
def test_check_output(test_file):
assert check_output(["cat", str(test_file)], shell=False) == "foo\nbar\n"
assert check_output(["cat", str(test_file)], shell=False) == "foo\nbar"
assert check_output("cat %s" % str(test_file)) == "foo\nbar\n"
assert check_output("cat %s" % str(test_file)) == "foo\nbar"

30
tox.ini
View file

@ -1,33 +1,21 @@
[tox]
envlist =
py35
lint
envlist =
py{27,3}-{pytest,lint}
format
format-check
docs
skipdist = True
[testenv]
usedevelop = True
passenv = *
extras = tests
deps =
pytest >= 4.6.3, < 5.0
pytest-cov >= 2.7.1, < 3.0
pytest-mock >= 1.10.4, < 2.0
pytest-env >= 0.6.2, < 1.0
requests >= 2.22.0, < 3.0
requests-mock >= 1.6.0, < 2.0
toml >= 0.10, < 0.11
gevent-websocket
bottle >= 0.12
WebTest >= 2.0, < 2.1
py{27,3}-pytest: .[tests]
py{27,3}-lint: flake8
commands =
pytest {posargs}
[testenv:lint]
commands = flake8 moulinette test
deps = flake8
skip_install = True
usedevelop = False
py{27,3}-pytest: pytest {posargs} -c pytest.ini
py{27,3}-lint: flake8 moulinette test
[testenv:format]
basepython = python3