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 - slapd
matrix: matrix:
allow_failures:
- env: TOXENV=py37-pytest
- env: TOXENV=py37-lint
include: include:
- python: 3.5 - python: 2.7
env: TOXENV=py35 env: TOXENV=py27-pytest
- python: 3.5 - python: 2.7
env: TOXENV=lint env: TOXENV=py27-lint
- python: 3.6 - python: 3.7
env: TOXENV=py37-pytest
- python: 3.7
env: TOXENV=py37-lint
- python: 3.7
env: TOXENV=format-check env: TOXENV=format-check
- python: 3.5 - python: 3.5
env: TOXENV=docs env: TOXENV=docs

136
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 moulinette (3.7.0.2) stable; urgency=low
Bumping version number for stable release 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) - [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 -- 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 # ~ Major stuff
- [enh] Add group and permission mechanism ([Moulinette#189](https://github.com/YunoHost/moulinette/pull/189) - [enh] Add group and permission mechanism (#189)
- [mod] Be able to customize prompt colors ([Moulinette/808f620](https://github.com/YunoHost/Moulinette/commit/808f620)) - [mod] Be able to customize prompt colors (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] Support app manifests in toml (#204, 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] 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 - [i18n] Improved translations for Catalan, Occitan, French, Arabic, Spanish, German, Norwegian Bokmål
# Smaller or pretty technical fix/enh # 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] 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 ([Moulinette/2e2e627](https://github.com/YunoHost/Moulinette/commit/2e2e627)) - [enh] Add a write_to_yaml utility similar to write_to_json (2e2e627)
- [enh] Warn the user about long locks ([Moulinette#205](https://github.com/YunoHost/moulinette/pull/205)) - [enh] Warn the user about long locks (#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)) - [mod] Tweak stuff about setuptools and moulinette deps? (b739f27, da00fc9, d8cbbb0)
- [fix] Misc micro bugfixes or improvements ([Moulinette/83d9e77](https://github.com/YunoHost/Moulinette/commit/83d9e77)) - [fix] Misc micro bugfixes or improvements (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)) - [doc] Fix doc building + add doc build tests with Tox (f1ac5b8, df7d478, 74c8f79, bcf92c7, af2c80c, d52a574, 307f660, dced104, ed3823b)
- [enh] READMEs improvements ([Moulinette/1541b74](https://github.com/YunoHost/Moulinette/commit/1541b74), [Moulinette/ad1eeef](https://github.com/YunoHost/Moulinette/commit/ad1eeef)) - [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 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) * [fix] Fix case where stdinfo is not provided in call_async_output (0a300e5)
* [i18n] Improve translation for Greek, Hungarian, Polish, Swedish, French, Catalan, Occitan * [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 -- Alexandre Aubin <alex.aubin@mailoo.org> Wed, 03 Apr 2019 02:25:00 +0000
@ -201,7 +303,7 @@ moulinette (2.7.13) testing; urgency=low
* [i18n] Improve translations for Portugueuse, Occitan * [i18n] Improve translations for Portugueuse, Occitan
* [enh] Add read_yaml util (#161) * [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 -- 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 * [fix] Avoid cases where get_cookie returns None
* [mod] Improve exception logging in ldap stuff * [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 -- 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) * [enh] Show description of command in --help (#148)
* [i18n] Update French translation (#149) * [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 -- Alexandre Aubin <alex.aubin@mailoo.org> Mon, 07 Aug 2017 13:04:21 -0400
@ -309,7 +411,7 @@ moulinette (2.6.0) testing; urgency=low
* [fix] Show positional arguments first in --help / usage (#138) * [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 -- 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 Source: moulinette
Section: python Section: python
Priority: optional 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) Build-Depends: debhelper (>= 9), python (>= 2.7), dh-python, python-setuptools, python-psutil, python-all (>= 2.7)
Standards-Version: 3.9.6 Standards-Version: 3.9.6
X-Python-Version: >= 2.7 X-Python-Version: >= 2.7
@ -18,12 +18,10 @@ Depends: ${misc:Depends}, ${python:Depends},
python-toml, python-toml,
python-psutil, python-psutil,
python-tz python-tz
Replaces: yunohost-cli Breaks: yunohost (<< 4.1)
Breaks: yunohost-cli
Description: prototype interfaces with ease in Python Description: prototype interfaces with ease in Python
The moulinette is a Python package that allows one to quickly and Quickly and easily prototype interfaces for your application.
easily prototype interfaces for your application. Each action can Each action can be served through an HTTP API and from the
be served through an HTTP API and from the command-line with a single command-line with a single method.
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_password": "كلمة السر خاطئة",
"invalid_usage": "إستعمال غير صالح، إستخدم --help لعرض المساعدة", "invalid_usage": "إستعمال غير صالح، إستخدم --help لعرض المساعدة",
"ldap_attribute_already_exists": "الخاصية '{attribute}' موجودة مسبقا و تحمل القيمة '{value}'", "ldap_attribute_already_exists": "الخاصية '{attribute}' موجودة مسبقا و تحمل القيمة '{value}'",
"ldap_operation_error": "طرأ هناك خطأ أثناء عملية في LDAP",
"ldap_server_down": "لا يمكن الإتصال بخادم LDAP", "ldap_server_down": "لا يمكن الإتصال بخادم LDAP",
"logged_in": "مُتّصل", "logged_in": "مُتّصل",
"logged_out": "تم تسجيل خروجك", "logged_out": "تم تسجيل خروجك",
@ -53,6 +52,9 @@
"corrupted_yaml": "قراءة مُشوّهة لنسق yaml مِن {ressource:s} (السبب : {error:s})", "corrupted_yaml": "قراءة مُشوّهة لنسق yaml مِن {ressource:s} (السبب : {error:s})",
"info": "معلومة:", "info": "معلومة:",
"warn_the_user_about_waiting_lock_again": "جارٍ الانتظار…", "warn_the_user_about_waiting_lock_again": "جارٍ الانتظار…",
"warn_the_user_that_lock_is_acquired": "لقد انتهى تنفيذ ذاك الأمر ، جارٍ إطلاق الأمر", "warn_the_user_that_lock_is_acquired": "لقد انتهى تنفيذ ذاك الأمر للتوّ ، جارٍ تنفيذ هذا الأمر",
"warn_the_user_about_waiting_lock": "هناك أمر لـ YunoHost قيد التشغيل حاليا. في انتظار انتهاء تنفيذه قبل تشغيل التالي" "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_password": "Contrasenya invàlida",
"invalid_usage": "Utilització invàlida, utilitzeu --help per veure l'ajuda", "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_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", "ldap_server_down": "No s'ha pogut connectar amb el servidor LDAP",
"logged_in": "Sessió iniciada", "logged_in": "Sessió iniciada",
"logged_out": "Sessió tancada", "logged_out": "Sessió tancada",
@ -40,21 +39,23 @@
"cannot_open_file": "No s'ha pogut obrir el fitxer {file:s} (motiu: {error:s})", "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})", "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})", "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_json": "JSON corrupte llegit des de {ressource:s} (motiu: {error:s})",
"corrupted_yaml": "Yaml 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_writing_file": "Error al escriure el fitxer {file:s}: {error:s}",
"error_removing": "Error al eliminar {path: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}", "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_ssl_error": "Error SSL al connectar amb {url:s}",
"download_timeout": "{url:s} ha tardat massa en respondre, s'ha deixat d'esperar.", "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_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}", "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:", "info": "Info:",
"corrupted_toml": "El fitxer TOML ha estat corromput en la lectura des de {ressource:s} (motiu: {error:s})", "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": "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_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", "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" "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_exists": "目录已存在:{path}",
"folder_not_exist": "目录不存在", "folder_not_exist": "目录不存在",
"info": "信息:", "info": "信息:",
"instance_already_running": "实例已正在运行", "instance_already_running": "已经有一个YunoHost操作正在运行。 请等待它完成再运行另一个。",
"invalid_argument": "参数错误{argument}{error}", "invalid_argument": "参数错误{argument}{error}",
"invalid_password": "密码错误", "invalid_password": "密码错误",
"invalid_usage": "用法错误,输入 --help 查看帮助信息", "invalid_usage": "用法错误,输入 --help 查看帮助信息",
"ldap_attribute_already_exists": "参数{attribute}已赋值{value}", "ldap_attribute_already_exists": "参数{attribute}已赋值{value}",
"ldap_operation_error": "LDAP操作时发生了错误",
"ldap_server_down": "无法连接LDAP服务器", "ldap_server_down": "无法连接LDAP服务器",
"logged_in": "登录成功", "logged_in": "登录",
"logged_out": "登出", "logged_out": "登出",
"not_logged_in": "您未登录", "not_logged_in": "您未登录",
"operation_interrupted": "操作中断", "operation_interrupted": "操作中断",
@ -31,7 +30,7 @@
"server_already_running": "服务已运行在指定端口", "server_already_running": "服务已运行在指定端口",
"success": "成功!", "success": "成功!",
"unable_authenticate": "认证失败", "unable_authenticate": "认证失败",
"unable_retrieve_session": "获取会话失败", "unable_retrieve_session": "由于“ {exception}”,无法检索会话",
"unknown_group": "未知组{group}", "unknown_group": "未知组{group}",
"unknown_user": "未知用户{user}", "unknown_user": "未知用户{user}",
"values_mismatch": "值不匹配", "values_mismatch": "值不匹配",
@ -39,16 +38,23 @@
"websocket_request_expected": "期望一个WebSocket请求", "websocket_request_expected": "期望一个WebSocket请求",
"cannot_open_file": "不能打开文件{file:s}(原因:{error:s}", "cannot_open_file": "不能打开文件{file:s}(原因:{error:s}",
"cannot_write_file": "写入文件{file:s}失败(原因:{error:s}", "cannot_write_file": "写入文件{file:s}失败(原因:{error:s}",
"unknown_error_reading_file": "尝试读取文件{file:s}时发生错误", "unknown_error_reading_file": "尝试读取文件{files}时发生未知错误(原因:{errors}",
"corrupted_json": "json数据{ressource:s}读取失败(原因:{error:s}", "corrupted_json": "从{ressource:s}读取的JSON损坏(原因:{error:s}",
"corrupted_yaml": "读取yaml文件{ressource:s}失败(原因:{error:s}", "corrupted_yaml": "从{ressource:s}读取的YMAL损坏(原因:{error:s}",
"error_writing_file": "写入文件{file:s}失败:{error:s}", "error_writing_file": "写入文件{file:s}失败:{error:s}",
"error_removing": "删除路径{path:s}失败:{error:s}", "error_removing": "删除路径{path:s}失败:{error:s}",
"error_changing_file_permissions": "目录{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_ssl_error": "连接{url:s}时发生SSL错误",
"download_timeout": "{url:s}响应超时,放弃。", "download_timeout": "{url:s}响应超时,放弃。",
"download_unknown_error": "下载{url:s}失败:{error:s}", "download_unknown_error": "下载{url:s}失败:{error:s}",
"download_bad_status_code": "{url:s}返回状态码:{code: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_password": "Passwort falsch",
"invalid_usage": "Falscher Aufruf, verwende --help für den Hilfstext", "invalid_usage": "Falscher Aufruf, verwende --help für den Hilfstext",
"ldap_attribute_already_exists": "Attribute existieren bereits: '{attribute}={value}'", "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", "ldap_server_down": "LDAP-Server nicht erreichbar",
"logged_in": "Angemeldet", "logged_in": "Angemeldet",
"logged_out": "Abgemeldet", "logged_out": "Abgemeldet",
@ -37,5 +36,26 @@
"deprecated_command_alias": "'{prog} {old}' ist veraltet und wird bald entfernt werden, benutze '{prog} {new}' stattdessen", "deprecated_command_alias": "'{prog} {old}' ist veraltet und wird bald entfernt werden, benutze '{prog} {new}' stattdessen",
"unknown_group": "Gruppe '{group}' ist unbekannt", "unknown_group": "Gruppe '{group}' ist unbekannt",
"unknown_user": "Benutzer '{user}' 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_token": "Invalid token - please authenticate",
"invalid_usage": "Invalid usage, pass --help to see help", "invalid_usage": "Invalid usage, pass --help to see help",
"ldap_attribute_already_exists": "Attribute '{attribute}' already exists with value '{value}'", "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", "ldap_server_down": "Unable to reach LDAP server",
"logged_in": "Logged in", "logged_in": "Logged in",
"logged_out": "Logged out", "logged_out": "Logged out",
@ -32,6 +31,7 @@
"success": "Success!", "success": "Success!",
"unable_authenticate": "Unable to authenticate", "unable_authenticate": "Unable to authenticate",
"unable_retrieve_session": "Unable to retrieve the session because '{exception}'", "unable_retrieve_session": "Unable to retrieve the session because '{exception}'",
"session_expired": "The session expired. Please re-authenticate.",
"unknown_group": "Unknown '{group}' group", "unknown_group": "Unknown '{group}' group",
"unknown_user": "Unknown '{user}' user", "unknown_user": "Unknown '{user}' user",
"values_mismatch": "Values don't match", "values_mismatch": "Values don't match",
@ -41,19 +41,20 @@
"cannot_open_file": "Could not open file {file:s} (reason: {error:s})", "cannot_open_file": "Could not open file {file:s} (reason: {error:s})",
"cannot_write_file": "Could not write 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})", "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_json": "Corrupted JSON read from {ressource:s} (reason: {error:s})",
"corrupted_yaml": "Corrupted yaml 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})", "corrupted_toml": "Corrupted TOML read from {ressource:s} (reason: {error:s})",
"error_writing_file": "Error when writing file {file:s}: {error:s}", "error_writing_file": "Error when writing file {file:s}: {error:s}",
"error_removing": "Error when removing {path: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}", "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_ssl_error": "SSL error when connecting to {url:s}",
"download_timeout": "{url:s} took too long to answer, gave up.", "download_timeout": "{url:s} took too long to answer, gave up.",
"download_unknown_error": "Error when downloading data from {url:s}: {error:s}", "download_unknown_error": "Error when downloading data from {url:s}: {error:s}",
"download_bad_status_code": "{url:s} returned status code {code: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": "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_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", "password": "Pasvorto",
"colon": "{}: ", "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_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", "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?", "command_unknown": "Komando '{command:s}' nekonata?",
@ -34,7 +34,6 @@
"not_logged_in": "Vi ne estas ensalutinta", "not_logged_in": "Vi ne estas ensalutinta",
"logged_in": "Ensalutinta", "logged_in": "Ensalutinta",
"ldap_server_down": "Ne eblas atingi la servilon LDAP", "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}'", "ldap_attribute_already_exists": "Atributo '{attribute}' jam ekzistas kun valoro '{value}'",
"invalid_usage": "Nevalida uzado, preterpase '--help' por vidi helpon", "invalid_usage": "Nevalida uzado, preterpase '--help' por vidi helpon",
"invalid_password": "Nevalida pasvorto", "invalid_password": "Nevalida pasvorto",

View file

@ -18,7 +18,6 @@
"invalid_password": "Contraseña no válida", "invalid_password": "Contraseña no válida",
"invalid_usage": "Uso no válido, utilice --help para ver la ayuda", "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_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", "ldap_server_down": "No se pudo conectar con el servidor LDAP",
"logged_in": "Sesión iniciada", "logged_in": "Sesión iniciada",
"logged_out": "Sesión cerrada", "logged_out": "Sesión cerrada",
@ -40,21 +39,23 @@
"cannot_open_file": "No se pudo abrir el archivo {file:s} (motivo: {error:s})", "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})", "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})", "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_writing_file": "Error al escribir el archivo {file:s}: {error:s}",
"error_removing": "Error al eliminar {path: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}", "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_ssl_error": "Error SSL al conectar con {url:s}",
"download_timeout": "{url:s} tardó demasiado en responder, abandono.", "download_timeout": "{url:s} tardó demasiado en responder, abandono.",
"download_unknown_error": "Error al descargar datos desde {url:s} : {error:s}", "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}", "download_bad_status_code": "{url:s} devolvió el código de estado {code:s}",
"command_unknown": "¿Orden «{command:s}» desconocida?", "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:", "info": "Información:",
"corrupted_toml": "Lectura corrupta de TOML desde {ressource:s} (motivo: {error:s})", "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_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", "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_password": "Mot de passe incorrect",
"invalid_usage": "Utilisation erronée, utilisez --help pour accéder à laide", "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_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", "ldap_server_down": "Impossible datteindre le serveur LDAP",
"logged_in": "Connecté", "logged_in": "Connecté",
"logged_out": "Déconnecté", "logged_out": "Déconnecté",
@ -55,6 +54,8 @@
"corrupted_toml": "Fichier TOML corrompu en lecture depuis {ressource:s} (cause : {error:s})", "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": "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_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", "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" "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_password": "अवैध पासवर्ड",
"invalid_usage": "अवैध उपयोग, सहायता देखने के लिए --help साथ लिखे।", "invalid_usage": "अवैध उपयोग, सहायता देखने के लिए --help साथ लिखे।",
"ldap_attribute_already_exists": "'{attribute}' तर्क पहले इस वैल्यू '{value}' से मौजूद है।", "ldap_attribute_already_exists": "'{attribute}' तर्क पहले इस वैल्यू '{value}' से मौजूद है।",
"ldap_operation_error": "LDAP ऑपरेशन के दौरान त्रुटि हो गई है।",
"ldap_server_down": "LDAP सर्वर तक पहुंचने में असमर्थ।", "ldap_server_down": "LDAP सर्वर तक पहुंचने में असमर्थ।",
"logged_in": "लोग्ड इन", "logged_in": "लोग्ड इन",
"logged_out": "लॉग आउट", "logged_out": "लॉग आउट",

View file

@ -20,7 +20,6 @@
"invalid_password": "Password non valida", "invalid_password": "Password non valida",
"invalid_usage": "Utilizzo non valido, usa --help per vedere l'aiuto", "invalid_usage": "Utilizzo non valido, usa --help per vedere l'aiuto",
"ldap_attribute_already_exists": "L'attributo '{attribute}' esiste già con valore '{value}'", "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", "ldap_server_down": "Impossibile raggiungere il server LDAP",
"logged_in": "Connesso", "logged_in": "Connesso",
"not_logged_in": "Non hai effettuato l'accesso", "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_open_file": "Impossibile aprire il file {file:s} (motivo: {error:s})",
"cannot_write_file": "Impossibile scrivere 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})", "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_json": "Lettura JSON corrotta da {resource:s} (motivo: {error:s})",
"corrupted_yaml": "Lettura yaml corrotta da {ressource: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_writing_file": "Errore durante la scrittura del file {file:s}: {error:s}",
"error_removing": "Errore durante la rimozione {path: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}", "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_ssl_error": "Errore SSL durante la connessione a {url:s}",
"download_timeout": "{url:s} ci ha messo troppo a rispondere, abbandonato.", "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_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}", "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:", "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_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", "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})", "corrupted_toml": "TOML corrotto da {ressource:s} (motivo: {errore:s})",
"invalid_token": "Token non valido: autenticare" "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_password": "Ongeldig wachtwoord",
"invalid_usage": "Ongeldig gebruik, doe --help om de hulptekst te lezen", "invalid_usage": "Ongeldig gebruik, doe --help om de hulptekst te lezen",
"ldap_attribute_already_exists": "Attribuut '{attribute}' bestaat al met waarde '{value}'", "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", "ldap_server_down": "Kan LDAP server niet bereiken",
"logged_in": "Ingelogd", "logged_in": "Ingelogd",
"logged_out": "Uitgelogd", "logged_out": "Uitgelogd",
@ -50,7 +49,7 @@
"download_unknown_error": "Fout tijdens het downloaden van data van {url:s}: {error:s}", "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}", "download_bad_status_code": "{url:s} stuurt status code {code:s}",
"command_unknown": "Opdracht '{command:s}' ongekend ?", "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_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", "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})", "corrupted_toml": "Ongeldige TOML werd gelezen op {ressource:s} (reason: {error:s})",

View file

@ -31,7 +31,6 @@
"warning": "Atencion:", "warning": "Atencion:",
"invalid_usage": "Usatge invalid, utilizatz --help per accedir a lajuda", "invalid_usage": "Usatge invalid, utilizatz --help per accedir a lajuda",
"ldap_attribute_already_exists": "Latribut « {attribute} » existís ja amb la valor: {value}", "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", "operation_interrupted": "Operacion interrompuda",
"server_already_running": "Un servidor es ja en execucion sus aqueste pòrt", "server_already_running": "Un servidor es ja en execucion sus aqueste pòrt",
"success": "Capitada!", "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": "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_about_waiting_lock_again": "Encara en espèra…",
"warn_the_user_that_lock_is_acquired": "lautra comanda ven dacabar, ara lançament daquesta comanda", "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", "logged_out": "Wylogowano",
"password": "hasło", "password": "Hasło",
"warn_the_user_that_lock_is_acquired": "drugie polecenie właśnie się zakończyło, teraz uruchamia to polecenie", "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_again": "Wciąż czekam...",
"warn_the_user_about_waiting_lock": "Kolejne polecenie YunoHost jest teraz uruchomione, czekamy na jego zakończenie przed uruchomieniem tego", "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?", "command_unknown": "Polecenie „{command:s}” jest nieznane?",
@ -34,7 +34,6 @@
"not_logged_in": "Nie jesteś zalogowany", "not_logged_in": "Nie jesteś zalogowany",
"logged_in": "Zalogowany", "logged_in": "Zalogowany",
"ldap_server_down": "Nie można połączyć się z serwerem LDAP", "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}”", "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_usage": "Nieprawidłowe użycie. Przejdź --help, aby wyświetlić pomoc",
"invalid_token": "Nieprawidłowy token - proszę uwierzytelnić", "invalid_token": "Nieprawidłowy token - proszę uwierzytelnić",

View file

@ -15,7 +15,6 @@
"invalid_password": "Senha incorreta", "invalid_password": "Senha incorreta",
"invalid_usage": "Uso invalido, utilizar --help para ver a ajuda", "invalid_usage": "Uso invalido, utilizar --help para ver a ajuda",
"ldap_attribute_already_exists": "O atributo '{attribute}' já existe com valor '{value}'", "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", "ldap_server_down": "Não foi possível comunicar com o servidor LDAP",
"logged_in": "Sessão iniciada", "logged_in": "Sessão iniciada",
"logged_out": "Sessão terminada", "logged_out": "Sessão terminada",
@ -43,18 +42,20 @@
"error_writing_file": "Erro ao gravar arquivo {file:s}: {error:s}", "error_writing_file": "Erro ao gravar arquivo {file:s}: {error:s}",
"error_removing": "Erro ao remover {path: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}", "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_ssl_error": "Erro de SSL ao conectar-se a {url:s}",
"download_timeout": "{url:s} demorou muito para responder, desistiu.", "download_timeout": "{url:s} demorou muito para responder, desistiu.",
"download_unknown_error": "Erro quando baixando os dados de {url:s} : {error:s}", "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}", "download_bad_status_code": "{url:s} retornou o código de status {code:s}",
"command_unknown": "Comando '{command:s}' desconhecido ?", "command_unknown": "Comando '{command:s}' desconhecido ?",
"corrupted_json": "Json corrompido lido do {ressource:s} (motivo: {error:s})", "corrupted_json": "JSON corrompido lido do {ressource:s} (motivo: {error:s})",
"corrupted_yaml": "Yaml 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_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_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", "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", "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_timeout": "Превышено время ожидания ответа от {url:s}.",
"download_unknown_error": "Ошибка при загрузке данных с {url:s} : {error:s}", "download_unknown_error": "Ошибка при загрузке данных с {url:s} : {error:s}",
"instance_already_running": "Операция YunoHost уже запущена. Пожалуйста, подождите, пока он закончится, прежде чем запускать другой.", "instance_already_running": "Операция YunoHost уже запущена. Пожалуйста, подождите, пока он закончится, прежде чем запускать другой.",
"ldap_operation_error": "Ошибка в процессе работы LDAP",
"root_required": "Чтобы выполнить это действие, вы должны иметь права root", "root_required": "Чтобы выполнить это действие, вы должны иметь права root",
"corrupted_json": "Повреждённый json получен от {ressource:s} (причина: {error:s})", "corrupted_json": "Повреждённый json получен от {ressource:s} (причина: {error:s})",
"command_unknown": "Команда '{command:s}' неизвестна ?", "command_unknown": "Команда '{command:s}' неизвестна ?",

View file

@ -49,7 +49,6 @@
"unable_retrieve_session": "Det gick inte att hämta sessionen eftersom '{exception}'", "unable_retrieve_session": "Det gick inte att hämta sessionen eftersom '{exception}'",
"unable_authenticate": "Det går inte att verifiera", "unable_authenticate": "Det går inte att verifiera",
"ldap_server_down": "Det går inte att nå LDAP-servern", "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_usage": "Ogiltig användning, pass --help för att se hjälp",
"invalid_token": "Ogiltigt token - verifiera", "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.", "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_argument": "Geçersiz argüman '{argument}': {error}",
"invalid_password": "Geçersiz parola", "invalid_password": "Geçersiz parola",
"ldap_attribute_already_exists": "'{attribute}={value}' özelliği zaten mevcut", "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", "ldap_server_down": "LDAP sunucusuna erişilemiyor",
"logged_in": "Giriş yapıldı", "logged_in": "Giriş yapıldı",
"logged_out": ıkış yapıldı", "logged_out": ıkış yapıldı",

View file

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

View file

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

View file

@ -5,7 +5,7 @@ import logging
import hashlib import hashlib
import hmac 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 from moulinette.core import MoulinetteError
logger = logging.getLogger("moulinette.authenticator") logger = logging.getLogger("moulinette.authenticator")
@ -159,6 +159,10 @@ class BaseAuthenticator(object):
"%s.asc" % session_id, mode, subdir="session/%s" % self.name "%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): def _store_session(self, session_id, session_token):
"""Store a session to be able to use it later to reauthenticate""" """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): def _authenticate_session(self, session_id, session_token):
"""Checks session and token against the stored session token""" """Checks session and token against the stored session token"""
if not self._session_exists(session_id):
raise MoulinetteError("session_expired")
try: try:
# FIXME : shouldn't we also add a check that this session file # FIXME : shouldn't we also add a check that this session file
# is not too old ? e.g. not older than 24 hours ? idk... # 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! # TODO: Use Python3 to remove this fix!
from __future__ import absolute_import from __future__ import absolute_import
import os
import logging import logging
import random
import string
import crypt
import ldap import ldap
import ldap.sasl import ldap.sasl
import time
import ldap.modlist as modlist 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 from moulinette.authenticators import BaseAuthenticator
logger = logging.getLogger("moulinette.authenticator.ldap") logger = logging.getLogger("moulinette.authenticator.ldap")
@ -69,7 +69,7 @@ class Authenticator(BaseAuthenticator):
# Implement virtual methods # Implement virtual methods
def authenticate(self, password=None): def authenticate(self, password=None):
try: def _reconnect():
con = ldap.ldapobject.ReconnectLDAPObject( con = ldap.ldapobject.ReconnectLDAPObject(
self._get_uri(), retry_max=10, retry_delay=0.5 self._get_uri(), retry_max=10, retry_delay=0.5
) )
@ -80,11 +80,23 @@ class Authenticator(BaseAuthenticator):
con.simple_bind_s(self.userdn, password) con.simple_bind_s(self.userdn, password)
else: else:
con.simple_bind_s() con.simple_bind_s()
return con
try:
con = _reconnect()
except ldap.INVALID_CREDENTIALS: except ldap.INVALID_CREDENTIALS:
raise MoulinetteError("invalid_password") raise MoulinetteError("invalid_password")
except ldap.SERVER_DOWN: except ldap.SERVER_DOWN:
logger.exception("unable to reach the server to authenticate") # ldap is down, attempt to restart it before really failing
raise MoulinetteError("ldap_server_down") 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 # Check that we are indeed logged in with the right identity
try: try:
@ -99,30 +111,6 @@ class Authenticator(BaseAuthenticator):
raise MoulinetteError("Not logged in with the expected userdn ?!") raise MoulinetteError("Not logged in with the expected userdn ?!")
else: else:
self.con = con 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 # Additional LDAP methods
# TODO: Review these methods # TODO: Review these methods
@ -148,15 +136,11 @@ class Authenticator(BaseAuthenticator):
try: try:
result = self.con.search_s(base, ldap.SCOPE_SUBTREE, filter, attrs) result = self.con.search_s(base, ldap.SCOPE_SUBTREE, filter, attrs)
except Exception as e: except Exception as e:
logger.exception( raise MoulinetteError(
"error during LDAP search operation with: base='%s', " "error during LDAP search operation with: base='%s', "
"filter='%s', attrs=%s and exception %s", "filter='%s', attrs=%s and exception %s" % (base, filter, attrs, e),
base, raw_msg=True,
filter,
attrs,
e,
) )
raise MoulinetteError("ldap_operation_error", action="search")
result_list = [] result_list = []
if not attrs or "dn" not in attrs: if not attrs or "dn" not in attrs:
@ -185,14 +169,11 @@ class Authenticator(BaseAuthenticator):
try: try:
self.con.add_s(dn, ldif) self.con.add_s(dn, ldif)
except Exception as e: except Exception as e:
logger.exception( raise MoulinetteError(
"error during LDAP add operation with: rdn='%s', " "error during LDAP add operation with: rdn='%s', "
"attr_dict=%s and exception %s", "attr_dict=%s and exception %s" % (rdn, attr_dict, e),
rdn, raw_msg=True,
attr_dict,
e,
) )
raise MoulinetteError("ldap_operation_error", action="add")
else: else:
return True return True
@ -211,12 +192,11 @@ class Authenticator(BaseAuthenticator):
try: try:
self.con.delete_s(dn) self.con.delete_s(dn)
except Exception as e: except Exception as e:
logger.exception( raise MoulinetteError(
"error during LDAP delete operation with: rdn='%s' and exception %s", "error during LDAP delete operation with: rdn='%s' and exception %s"
rdn, % (rdn, e),
e, raw_msg=True,
) )
raise MoulinetteError("ldap_operation_error", action="remove")
else: else:
return True return True
@ -249,15 +229,12 @@ class Authenticator(BaseAuthenticator):
self.con.modify_ext_s(dn, ldif) self.con.modify_ext_s(dn, ldif)
except Exception as e: except Exception as e:
logger.exception( raise MoulinetteError(
"error during LDAP update operation with: rdn='%s', " "error during LDAP update operation with: rdn='%s', "
"attr_dict=%s, new_rdn=%s and exception: %s", "attr_dict=%s, new_rdn=%s and exception: %s"
rdn, % (rdn, attr_dict, new_rdn, e),
attr_dict, raw_msg=True,
new_rdn,
e,
) )
raise MoulinetteError("ldap_operation_error", action="update")
else: else:
return True 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) cache_dir = get_cachedir(subdir, make_dir=True if mode[0] == "w" else False)
file_path = os.path.join(cache_dir, filename) file_path = os.path.join(cache_dir, filename)
return open(file_path, mode) 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 json
import logging import logging
from importlib import import_module
import moulinette import moulinette
from moulinette.globals import init_moulinette_env from moulinette.globals import init_moulinette_env
@ -372,53 +370,6 @@ class MoulinetteSignals(object):
raise NotImplementedError("this signal is not handled") 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 ---------------------------------------------- # Moulinette core classes ----------------------------------------------
@ -435,6 +386,10 @@ class MoulinetteError(Exception):
self.strerror = msg self.strerror = msg
class MoulinetteLdapIsDownError(MoulinetteError):
"""Used when ldap is down"""
class MoulinetteLock(object): class MoulinetteLock(object):
"""Locker for a moulinette instance """Locker for a moulinette instance

View file

@ -11,4 +11,7 @@ def init_moulinette_env():
"MOULINETTE_LOCALES_DIR", "/usr/share/moulinette/locale" "MOULINETTE_LOCALES_DIR", "/usr/share/moulinette/locale"
), ),
"CACHE_DIR": environ.get("MOULINETTE_CACHE_DIR", "/var/cache/moulinette"), "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_method = callback.get("method")
self.callback_kwargs = callback.get("kwargs", {}) self.callback_kwargs = callback.get("kwargs", {})
self.callback_return = callback.get("return", False) self.callback_return = callback.get("return", False)
logger.debug(
"registering new callback action '{0}' to {1}".format(
self.callback_method, option_strings
)
)
@property @property
def callback(self): def callback(self):
@ -361,6 +356,9 @@ class _CallbackAction(argparse.Action):
mod = __import__(mod_name, globals=globals(), level=0, fromlist=[func_name]) mod = __import__(mod_name, globals=globals(), level=0, fromlist=[func_name])
func = getattr(mod, func_name) func = getattr(mod, func_name)
except (AttributeError, ImportError): except (AttributeError, ImportError):
import traceback
traceback.print_exc()
raise ValueError("unable to import method {0}".format(self.callback_method)) raise ValueError("unable to import method {0}".format(self.callback_method))
self._callback = func self._callback = func

View file

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

View file

@ -7,12 +7,12 @@ import locale
import logging import logging
from argparse import SUPPRESS from argparse import SUPPRESS
from collections import OrderedDict from collections import OrderedDict
import pytz
from datetime import date, datetime from datetime import date, datetime
import argcomplete import argcomplete
from moulinette import msignals, m18n from moulinette import msignals, m18n
from moulinette.actionsmap import ActionsMap
from moulinette.core import MoulinetteError from moulinette.core import MoulinetteError
from moulinette.interfaces import ( from moulinette.interfaces import (
BaseActionsMapParser, BaseActionsMapParser,
@ -103,6 +103,8 @@ def pretty_date(_date):
Argument: Argument:
- date -- The date or datetime to display - date -- The date or datetime to display
""" """
import pytz # Lazy loading, this takes like 3+ sec on a RPi2 ?!
# Deduce system timezone # Deduce system timezone
nowutc = datetime.now(tz=pytz.utc) nowutc = datetime.now(tz=pytz.utc)
nowtz = datetime.now() nowtz = datetime.now()
@ -168,8 +170,13 @@ def pretty_print_dict(d, depth=0):
def get_locale(): def get_locale():
"""Return current user locale""" """Return current user eocale"""
try:
lang = locale.getdefaultlocale()[0] 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: if not lang:
return "" return ""
return lang[:2] 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 # Set user locale
m18n.set_locale(get_locale()) m18n.set_locale(get_locale())
@ -428,9 +436,12 @@ class Interface(BaseInterface):
msignals.set_handler("authenticate", self._do_authenticate) msignals.set_handler("authenticate", self._do_authenticate)
msignals.set_handler("prompt", self._do_prompt) 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 """Run the moulinette
Process the action corresponding to the given arguments 'args' Process the action corresponding to the given arguments 'args'
@ -442,7 +453,6 @@ class Interface(BaseInterface):
- json: return a JSON encoded string - json: return a JSON encoded string
- plain: return a script-readable output - plain: return a script-readable output
- none: do not output the result - 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 - 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,10 +463,6 @@ class Interface(BaseInterface):
argcomplete.autocomplete(self.actionsmap.parser._parser) argcomplete.autocomplete(self.actionsmap.parser._parser)
# Set handler for authentication # 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: try:
@ -489,7 +495,11 @@ class Interface(BaseInterface):
Handle the core.MoulinetteSignals.authenticate signal. 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") help = authenticator.extra.get("help")
msg = m18n.n(help) if help else m18n.g("password") msg = m18n.n(help) if help else m18n.g("password")
return authenticator(password=self._do_prompt(msg, True, False, color="yellow")) 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 # load configuration from dict
dictConfig(DEFAULT_LOGGING) dictConfig(DEFAULT_LOGGING)
if logging_config: if logging_config:
logging.main_logger = logging_config.get("main_logger")
dictConfig(logging_config) 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. 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 ---------------------------------------------- # Call with stream access ----------------------------------------------

View file

@ -1,7 +1,6 @@
import logging import logging
from json.encoder import JSONEncoder from json.encoder import JSONEncoder
import datetime import datetime
import pytz
logger = logging.getLogger("moulinette.utils.serialize") logger = logging.getLogger("moulinette.utils.serialize")
@ -23,6 +22,9 @@ class JSONExtendedEncoder(JSONEncoder):
""" """
def default(self, o): def default(self, o):
import pytz # Lazy loading, this takes like 3+ sec on a RPi2 ?!
"""Return a serializable object""" """Return a serializable object"""
# Convert compatible containers into list # Convert compatible containers into list
if isinstance(o, set) or (hasattr(o, "__iter__") and hasattr(o, "next")): 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'): if f.endswith('.json'):
locale_files.append('locales/%s' % f) 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', setup(name='Moulinette',
version='2.0.0', version='2.0.0',
@ -27,24 +52,8 @@ setup(name='Moulinette',
license='AGPL', license='AGPL',
packages=find_packages(exclude=['test']), packages=find_packages(exclude=['test']),
data_files=[(LOCALES_DIR, locale_files)], data_files=[(LOCALES_DIR, locale_files)],
python_requires='>=3.5', python_requires='>=2.7.*, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4',
install_requires=[ install_requires=install_deps,
'argcomplete', tests_require=test_deps,
'psutil', extras_require=extras,
'pytz',
'pyyaml',
'toml',
'python-ldap',
'gevent-websocket',
'bottle',
],
tests_require=[
'pytest',
'pytest-cov',
'pytest-env',
'pytest-mock',
'requests',
'requests-mock',
'webtest'
],
) )

View file

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

View file

@ -98,7 +98,23 @@ class LDAPServer:
"posixAccount", "posixAccount",
"simpleSecurityObject", "simpleSecurityObject",
], ],
"userPassword": ["yunohost"], "userPassword": [self._hash_user_password("yunohost")],
} }
ldap_interface.update("cn=admin", admin_dict) 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): def test_actions_map_unknown_authenticator(monkeypatch, tmp_path):
monkeypatch.setenv("MOULINETTE_DATA_DIR", str(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() actionsmap_dir.mkdir()
amap = ActionsMap(BaseActionsMapParser) amap = ActionsMap(BaseActionsMapParser())
with pytest.raises(ValueError) as exception: with pytest.raises(ValueError) as exception:
amap.get_authenticator_for_profile("unknown") amap.get_authenticator_for_profile("unknown")
assert "Unknown authenticator" in str(exception) assert "Unknown authenticator" in str(exception)
@ -225,7 +225,7 @@ def test_extra_argument_parser_parse_args(iface, mocker):
def test_actions_map_api(): def test_actions_map_api():
from moulinette.interfaces.api import ActionsMapParser from moulinette.interfaces.api import ActionsMapParser
amap = ActionsMap(ActionsMapParser, use_cache=False) amap = ActionsMap(ActionsMapParser())
assert amap.parser.global_conf["authenticate"] == "all" assert amap.parser.global_conf["authenticate"] == "all"
assert "default" in amap.parser.global_conf["authenticator"] 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 ("GET", "/test-auth/default") in amap.parser.routes
assert ("POST", "/test-auth/subcat/post") 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 amap.parser.global_conf["authenticate"] == "all"
assert "default" in amap.parser.global_conf["authenticator"] assert "default" in amap.parser.global_conf["authenticator"]
@ -247,17 +247,24 @@ def test_actions_map_api():
def test_actions_map_import_error(mocker): def test_actions_map_import_error(mocker):
from moulinette.interfaces.api import ActionsMapParser from moulinette.interfaces.api import ActionsMapParser
amap = ActionsMap(ActionsMapParser) amap = ActionsMap(ActionsMapParser())
from moulinette.core import MoulinetteLock from moulinette.core import MoulinetteLock
mocker.patch.object(MoulinetteLock, "_is_son_of", return_value=False) 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: with pytest.raises(MoulinetteError) as exception:
amap.process({}, timeout=30, route=("GET", "/test-auth/none")) amap.process({}, timeout=30, route=("GET", "/test-auth/none"))
mocker.stopall()
translation = m18n.g("error_see_log") translation = m18n.g("error_see_log")
expected_msg = translation.format() expected_msg = translation.format()
assert expected_msg in str(exception) assert expected_msg in str(exception)
@ -274,9 +281,7 @@ def test_actions_map_cli():
default=False, default=False,
help="Log and print debug messages", help="Log and print debug messages",
) )
amap = ActionsMap( amap = ActionsMap(ActionsMapParser(top_parser=parser))
ActionsMapParser, use_cache=False, parser_kwargs={"top_parser": parser}
)
assert amap.parser.global_conf["authenticate"] == "all" assert amap.parser.global_conf["authenticate"] == "all"
assert "default" in amap.parser.global_conf["authenticator"] assert "default" in amap.parser.global_conf["authenticator"]
@ -293,11 +298,9 @@ def test_actions_map_cli():
.choices .choices
) )
amap.generate_cache() amap.generate_cache("moulitest")
amap = ActionsMap( amap = ActionsMap(ActionsMapParser(top_parser=parser))
ActionsMapParser, use_cache=True, parser_kwargs={"top_parser": parser}
)
assert amap.parser.global_conf["authenticate"] == "all" assert amap.parser.global_conf["authenticate"] == "all"
assert "default" in amap.parser.global_conf["authenticator"] assert "default" in amap.parser.global_conf["authenticator"]

View file

@ -216,18 +216,15 @@ class TestAuthCLI:
assert "some_data_from_default" in message.out assert "some_data_from_default" in message.out
moulinette_cli.run( moulinette_cli.run(["testauth", "default"], output_as="plain")
["testauth", "default"], output_as="plain", password="default"
)
message = capsys.readouterr() message = capsys.readouterr()
assert "some_data_from_default" in message.out assert "some_data_from_default" in message.out
def test_login_bad_password(self, moulinette_cli, capsys, mocker): def test_login_bad_password(self, moulinette_cli, capsys, mocker):
mocker.patch("getpass.getpass", return_value="Bad Password")
with pytest.raises(MoulinetteError): with pytest.raises(MoulinetteError):
moulinette_cli.run( moulinette_cli.run(["testauth", "default"], output_as="plain")
["testauth", "default"], output_as="plain", password="Bad Password"
)
mocker.patch("getpass.getpass", return_value="Bad Password") mocker.patch("getpass.getpass", return_value="Bad Password")
with pytest.raises(MoulinetteError): with pytest.raises(MoulinetteError):
@ -242,10 +239,9 @@ class TestAuthCLI:
expected_msg = translation.format() expected_msg = translation.format()
assert expected_msg in str(exception) assert expected_msg in str(exception)
mocker.patch("getpass.getpass", return_value="yoloswag")
with pytest.raises(MoulinetteError) as exception: with pytest.raises(MoulinetteError) as exception:
moulinette_cli.run( moulinette_cli.run(["testauth", "default"], output_as="none")
["testauth", "default"], output_as="none", password="yoloswag"
)
expected_msg = translation.format() expected_msg = translation.format()
assert expected_msg in str(exception) 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): 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" new_path = tmp_path / "new_folder"
permission = 0o700 permission = 0o700
mkdir(str(new_path), mode=permission) mkdir(str(new_path), mode=permission)

View file

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

28
tox.ini
View file

@ -1,33 +1,21 @@
[tox] [tox]
envlist = envlist =
py35 py{27,3}-{pytest,lint}
lint format
format-check
docs docs
skipdist = True skipdist = True
[testenv] [testenv]
usedevelop = True usedevelop = True
passenv = * passenv = *
extras = tests
deps = deps =
pytest >= 4.6.3, < 5.0 py{27,3}-pytest: .[tests]
pytest-cov >= 2.7.1, < 3.0 py{27,3}-lint: flake8
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
commands = commands =
pytest {posargs} py{27,3}-pytest: pytest {posargs} -c pytest.ini
py{27,3}-lint: flake8 moulinette test
[testenv:lint]
commands = flake8 moulinette test
deps = flake8
skip_install = True
usedevelop = False
[testenv:format] [testenv:format]
basepython = python3 basepython = python3