mirror of
https://github.com/YunoHost/moulinette.git
synced 2024-09-03 20:06:31 +02:00
Merge remote-tracking branch 'origin/dev' into enh-python3
This commit is contained in:
commit
bd27799283
44 changed files with 607 additions and 495 deletions
17
.travis.yml
17
.travis.yml
|
@ -7,12 +7,19 @@ addons:
|
|||
- slapd
|
||||
|
||||
matrix:
|
||||
allow_failures:
|
||||
- env: TOXENV=py37-pytest
|
||||
- env: TOXENV=py37-lint
|
||||
include:
|
||||
- python: 3.5
|
||||
env: TOXENV=py35
|
||||
- python: 3.5
|
||||
env: TOXENV=lint
|
||||
- python: 3.6
|
||||
- python: 2.7
|
||||
env: TOXENV=py27-pytest
|
||||
- python: 2.7
|
||||
env: TOXENV=py27-lint
|
||||
- python: 3.7
|
||||
env: TOXENV=py37-pytest
|
||||
- python: 3.7
|
||||
env: TOXENV=py37-lint
|
||||
- python: 3.7
|
||||
env: TOXENV=format-check
|
||||
- python: 3.5
|
||||
env: TOXENV=docs
|
||||
|
|
140
debian/changelog
vendored
140
debian/changelog
vendored
|
@ -1,3 +1,105 @@
|
|||
moulinette (4.1.0) testing; urgency=low
|
||||
|
||||
- [enh] Simplify interface initialization (Moulinette#245)
|
||||
- [enh] Be able to return a raw HTTP response (Moulinette#255)
|
||||
- [fix] Incorrect locale in some situations (Moulinette/d9fa6c7)
|
||||
- [fix] Prevent installing moulinette 4.1 without Yunohost 4.1 (Moulinette/9609fe1)
|
||||
- [fix] Error messages are not displayed in some situations (Moulinette/2501ecd)
|
||||
- Update translations for French, Spanish, Italian, Portuguese, Czech (Moulinette#256)
|
||||
|
||||
Thanks to all contributors <3 ! (ppr, KaeruCT, Omnia89, roukydesbois, miloskroulik, Aleks, Kay0u, miloskroulik)
|
||||
|
||||
-- Kay0u <pierre@kayou.io> Thu, 03 Dec 2020 16:32:44 +0100
|
||||
|
||||
moulinette (4.0.3) stable; urgency=low
|
||||
|
||||
- Bump version number for stable release
|
||||
|
||||
-- Alexandre Aubin <alex.aubin@mailoo.org> Wed, 29 Jul 2020 17:00:00 +0200
|
||||
|
||||
moulinette (4.0.2~beta) testing; urgency=low
|
||||
|
||||
- Bump version number for beta release
|
||||
|
||||
-- Alexandre Aubin <alex.aubin@mailoo.org> Fri, 19 Jun 2020 15:33:29 +0200
|
||||
|
||||
moulinette (4.0.1~alpha) testing; urgency=low
|
||||
|
||||
- [fix] Get rid of legacy code which breaks postinstall on buster for some reason (ac83b10f)
|
||||
- [fix] Remove legacy Breaks and Replaces (e49a47c7)
|
||||
- [fix] Let's hash the password like we do in core during tests (0c78374e)
|
||||
|
||||
-- Alexandre Aubin <alex.aubin@mailoo.org> Fri, 05 Jun 2020 17:32:35 +0200
|
||||
|
||||
moulinette (3.8.1.3) stable; urgency=low
|
||||
|
||||
- Update authorship/maintainer information (7818f07)
|
||||
- [i18n] Translations updated for Arabic, Catalan, French, Italian
|
||||
|
||||
Thanks to all contributors <3 ! (ButterflyOfFire, É. Gaspar, L. Noferini, ppr, xaloc33)
|
||||
|
||||
-- Alexandre Aubin <alex.aubin@mailoo.org> Mon, 27 Jul 2020 17:15:36 +0200
|
||||
|
||||
moulinette (3.8.1.2) stable; urgency=low
|
||||
|
||||
- [fix] locale parsing in some edge case
|
||||
- [i18n] Translations updated for Chinese (Simplified), Catalan, French, Nepali, Occitan
|
||||
|
||||
Thanks to all contributors ! (Aleks, amirale qt, clecle226, Quentí, xaloc33)
|
||||
|
||||
-- Kay0u <pierre@kayou.io> Fri, 22 May 2020 07:58:19 +0000
|
||||
|
||||
moulinette (3.8.1.1) stable; urgency=low
|
||||
|
||||
Bumping version number for stable release
|
||||
|
||||
-- Kay0u <pierre@kayou.io> Wed, 20 May 2020 18:56:36 +0000
|
||||
|
||||
moulinette (3.8.1) testing; urgency=low
|
||||
|
||||
- [fix] Misc technical ux/debugging fixes (#242, #243, #244, 840f27d2)
|
||||
- [fix] try to autorestart ldap when the server is down (#247)
|
||||
- [i18n] Translations updated for Dutch, Esperanto, French, German, Nepali, Polish
|
||||
|
||||
Thanks to all contributors <3 ! (amirale qt, Bram, É. Gaspar, Kay0u, M. Döring, Zeik0s)
|
||||
|
||||
-- Alexandre Aubin <alex.aubin@mailoo.org> Sat, 09 May 2020 21:09:35 +0200
|
||||
|
||||
moulinette (3.8.0) testing; urgency=low
|
||||
|
||||
# Major stuff
|
||||
|
||||
- Simplify auth mechanism (#216)
|
||||
- Add more tests (#230)
|
||||
- Use Black in Moulinette (#220, 6f5daa0, 54b8cab)
|
||||
|
||||
# Minor technical stuff
|
||||
|
||||
- [fix] Don't display comment if argument is already set (#226)
|
||||
- Don't miserably crash if async running can't read incoming message (06d8c48)
|
||||
- Report the actual error when ldap fails (628ffc9)
|
||||
|
||||
# i18n
|
||||
|
||||
- Improve translations for Swedish, Dutch, Italian, Russian, Polish, Portuguese, Catalan, Spanish, Occitan, Nepali, Esperanto, Basque, Chinese (Simplified), Arabic, German, Hungarian, Greek, Turkish, Bengali (Bangladesh)
|
||||
|
||||
Thanks to all contributors ! (Aleks, Bram, ButterflyOfFire, Filip B., Jeroen F., Josué T., Kay0u, Quentí, Yifei D., amirale qt, decentral1se, Elie G., frju365, Romain R., xaloc33)
|
||||
|
||||
-- Kay0u <pierre@kayou.io> Thu, 09 Apr 2020 20:29:48 +0000
|
||||
|
||||
moulinette (3.7.1.1) stable; urgency=low
|
||||
|
||||
- [fix] Report actual errors when some LDAP operation fails to ease
|
||||
debugging
|
||||
|
||||
-- Alexandre Aubin <alex.aubin@mailoo.org> Fri, 17 Apr 2020 17:00:00 +0000
|
||||
|
||||
moulinette (3.7.1) stable; urgency=low
|
||||
|
||||
- [enh] Lazy loading pytz for performances
|
||||
|
||||
-- Alexandre Aubin <alex.aubin@mailoo.org> Thu, 9 Apr 2020 14:55:00 +0000
|
||||
|
||||
moulinette (3.7.0.2) stable; urgency=low
|
||||
|
||||
Bumping version number for stable release
|
||||
|
@ -8,7 +110,7 @@ moulinette (3.7.0.1) testing; urgency=low
|
|||
|
||||
- [fix] Slapd may crash if we try to update the LDAP with no change (moulinette#231)
|
||||
|
||||
Thanks to all contributors (Josue) <3 !
|
||||
Thanks to all contributors (Josué) <3 !
|
||||
|
||||
-- Kay0u <pierre@kayou.io> Sun, 15 Mar 2020 16:09:25 +0000
|
||||
|
||||
|
@ -16,21 +118,21 @@ moulinette (3.7.0) testing; urgency=low
|
|||
|
||||
# ~ Major stuff
|
||||
|
||||
- [enh] Add group and permission mechanism ([Moulinette#189](https://github.com/YunoHost/moulinette/pull/189)
|
||||
- [mod] Be able to customize prompt colors ([Moulinette/808f620](https://github.com/YunoHost/Moulinette/commit/808f620))
|
||||
- [enh] Support app manifests in toml ([Moulinette#204](https://github.com/YunoHost/moulinette/pull/204), [Moulinette/55515cb](https://github.com/YunoHost/Moulinette/commit/55515cb))
|
||||
- [enh] Quite a lot of messages improvements, string cleaning, language rework... ([Moulinette/599bec3](https://github.com/YunoHost/Moulinette/commit/599bec3), [Moulinette#208](https://github.com/YunoHost/moulinette/pull/208), [Moulinette#213](https://github.com/YunoHost/moulinette/pull/213), [Moulinette/b7d415d](https://github.com/YunoHost/Moulinette/commit/b7d415d), [Moulinette/a8966b8](https://github.com/YunoHost/Moulinette/commit/a8966b8), [Moulinette/fdf9a71](https://github.com/YunoHost/Moulinette/commit/fdf9a71), [Moulinette/d895ae3](https://github.com/YunoHost/Moulinette/commit/d895ae3), [Moulinette/bdf0a1c](https://github.com/YunoHost/Moulinette/commit/bdf0a1c))
|
||||
- [enh] Add group and permission mechanism (#189)
|
||||
- [mod] Be able to customize prompt colors (808f620)
|
||||
- [enh] Support app manifests in toml (#204, 55515cb)
|
||||
- [enh] Quite a lot of messages improvements, string cleaning, language rework... (599bec3, #208, #213, b7d415d, a8966b8, fdf9a71, d895ae3, bdf0a1c)
|
||||
- [i18n] Improved translations for Catalan, Occitan, French, Arabic, Spanish, German, Norwegian Bokmål
|
||||
|
||||
# Smaller or pretty technical fix/enh
|
||||
|
||||
- [enh] Preparations for moulinette Python3 migration (Tox, Pytest and unit tests) ([Moulinette#203](https://github.com/YunoHost/moulinette/pull/203), [Moulinette#206](https://github.com/YunoHost/moulinette/pull/206), [Moulinette#207](https://github.com/YunoHost/moulinette/pull/207), [Moulinette#210](https://github.com/YunoHost/moulinette/pull/210), [Moulinette#211](https://github.com/YunoHost/moulinette/pull/211) [Moulinette#212](https://github.com/YunoHost/moulinette/pull/212), [Moulinette/2403ee1](https://github.com/YunoHost/Moulinette/commit/2403ee1), [Moulinette/69b0d49](https://github.com/YunoHost/Moulinette/commit/69b0d49), [Moulinette/49c749c](https://github.com/YunoHost/Moulinette/commit/49c749c), [Moulinette/2c84ee1](https://github.com/YunoHost/Moulinette/commit/2c84ee1), [Moulinette/cef72f7](https://github.com/YunoHost/Moulinette/commit/cef72f7))
|
||||
- [enh] Add a write_to_yaml utility similar to write_to_json ([Moulinette/2e2e627](https://github.com/YunoHost/Moulinette/commit/2e2e627))
|
||||
- [enh] Warn the user about long locks ([Moulinette#205](https://github.com/YunoHost/moulinette/pull/205))
|
||||
- [mod] Tweak stuff about setuptools and moulinette deps? ([Moulinette/b739f27](https://github.com/YunoHost/Moulinette/commit/b739f27), [Moulinette/da00fc9](https://github.com/YunoHost/Moulinette/commit/da00fc9), [Moulinette/d8cbbb0](https://github.com/YunoHost/Moulinette/commit/d8cbbb0))
|
||||
- [fix] Misc micro bugfixes or improvements ([Moulinette/83d9e77](https://github.com/YunoHost/Moulinette/commit/83d9e77))
|
||||
- [doc] Fix doc building + add doc build tests with Tox ([Moulinette/f1ac5b8](https://github.com/YunoHost/Moulinette/commit/f1ac5b8), [Moulinette/df7d478](https://github.com/YunoHost/Moulinette/commit/df7d478), [Moulinette/74c8f79](https://github.com/YunoHost/Moulinette/commit/74c8f79), [Moulinette/bcf92c7](https://github.com/YunoHost/Moulinette/commit/bcf92c7), [Moulinette/af2c80c](https://github.com/YunoHost/Moulinette/commit/af2c80c), [Moulinette/d52a574](https://github.com/YunoHost/Moulinette/commit/d52a574), [Moulinette/307f660](https://github.com/YunoHost/Moulinette/commit/307f660), [Moulinette/dced104](https://github.com/YunoHost/Moulinette/commit/dced104), [Moulinette/ed3823b](https://github.com/YunoHost/Moulinette/commit/ed3823b))
|
||||
- [enh] READMEs improvements ([Moulinette/1541b74](https://github.com/YunoHost/Moulinette/commit/1541b74), [Moulinette/ad1eeef](https://github.com/YunoHost/Moulinette/commit/ad1eeef))
|
||||
- [enh] Preparations for moulinette Python3 migration (Tox, Pytest and unit tests) (#203, #206, #207, #210, #211 #212, 2403ee1, 69b0d49, 49c749c, 2c84ee1, cef72f7)
|
||||
- [enh] Add a write_to_yaml utility similar to write_to_json (2e2e627)
|
||||
- [enh] Warn the user about long locks (#205)
|
||||
- [mod] Tweak stuff about setuptools and moulinette deps? (b739f27, da00fc9, d8cbbb0)
|
||||
- [fix] Misc micro bugfixes or improvements (83d9e77)
|
||||
- [doc] Fix doc building + add doc build tests with Tox (f1ac5b8, df7d478, 74c8f79, bcf92c7, af2c80c, d52a574, 307f660, dced104, ed3823b)
|
||||
- [enh] READMEs improvements (1541b74, ad1eeef)
|
||||
|
||||
Thanks to all contributors <3 ! (accross all repo: Yunohost, Moulinette, SSOwat, Yunohost-admin) : advocatux, Aksel K., Aleks, Allan N., amirale qt, Armin P., Bram, ButterflyOfFire, Carles S. A., chema o. r., decentral1se, Emmanuel V., Etienne M., Filip B., Geoff M., htsr, Jibec, Josué, Julien J., Kayou, liberodark, ljf, lucaskev, Lukas D., madtibo, Martin D., Mélanie C., nr 458 h, pitfd, ppr, Quentí, sidddy, troll, tufek yamero, xaloc33, yalh76
|
||||
|
||||
|
@ -80,7 +182,7 @@ moulinette (3.5.1) testing; urgency=low
|
|||
* [fix] Fix case where stdinfo is not provided in call_async_output (0a300e5)
|
||||
* [i18n] Improve translation for Greek, Hungarian, Polish, Swedish, French, Catalan, Occitan
|
||||
|
||||
Thanks to all contributors (Aleks, ariasuni, Quenti, ppr, Xaloc) <3 !
|
||||
Thanks to all contributors (Aleks, ariasuni, Quentí, ppr, Xaloc) <3 !
|
||||
|
||||
-- Alexandre Aubin <alex.aubin@mailoo.org> Wed, 03 Apr 2019 02:25:00 +0000
|
||||
|
||||
|
@ -88,7 +190,7 @@ moulinette (3.5.0) testing; urgency=low
|
|||
|
||||
* [i18n] Improve Russian and Chinese (Mandarin) translations
|
||||
|
||||
Contributors : n3uz, Алексей
|
||||
Contributors : n3uz, Алексей
|
||||
|
||||
-- Alexandre Aubin <alex.aubin@mailoo.org> Wed, 13 Mar 2019 17:20:00 +0000
|
||||
|
||||
|
@ -201,7 +303,7 @@ moulinette (2.7.13) testing; urgency=low
|
|||
* [i18n] Improve translations for Portugueuse, Occitan
|
||||
* [enh] Add read_yaml util (#161)
|
||||
|
||||
Contributors : Bram, by0ne, Quent-in
|
||||
Contributors : Bram, by0ne, Quentí
|
||||
|
||||
-- Alexandre Aubin <alex.aubin@mailoo.org> Mon, 28 May 2018 02:55:00 +0000
|
||||
|
||||
|
@ -219,7 +321,7 @@ moulinette (2.7.11) testing; urgency=low
|
|||
* [fix] Avoid cases where get_cookie returns None
|
||||
* [mod] Improve exception logging in ldap stuff
|
||||
|
||||
Thanks to all contributors (pitchum, Bram, ButteflyOfFire, J. Keerl, Matthieu, Jibec, David B, Quenti, bjarkan) <3 !
|
||||
Thanks to all contributors (pitchum, Bram, ButteflyOfFire, J. Keerl, Matthieu, Jibec, David B, Quentí, bjarkan) <3 !
|
||||
|
||||
-- Alexandre Aubin <alex.aubin@mailoo.org> Tue, 01 May 2018 23:33:59 +0000
|
||||
|
||||
|
@ -291,7 +393,7 @@ moulinette (2.7.0) testing; urgency=low
|
|||
* [enh] Show description of command in --help (#148)
|
||||
* [i18n] Update French translation (#149)
|
||||
|
||||
Thanks to all contributors (Bram, Aleks, R. Cabaret) ! <3
|
||||
Thanks to all contributors (Bram, Aleks, R. Cabaret) ! <3
|
||||
|
||||
-- Alexandre Aubin <alex.aubin@mailoo.org> Mon, 07 Aug 2017 13:04:21 -0400
|
||||
|
||||
|
@ -307,9 +409,9 @@ moulinette (2.6.0) testing; urgency=low
|
|||
|
||||
* [fix] Use ordered dict for the actionmap cache (#136)
|
||||
* [fix] Show positional arguments first in --help / usage (#138)
|
||||
* Update translations for Portuguese, German, Dutch
|
||||
* Update translations for Portuguese, German, Dutch
|
||||
|
||||
Thanks to all contributors and translators ! (Trollken, frju, Fabien Gruber, Jeroen Keerl, Aleks)
|
||||
Thanks to all contributors and translators ! (Trollken, frju, Fabien Gruber, Jeroen Keerl, Aleks)
|
||||
|
||||
-- Alexandre Aubin <alex.aubin@mailoo.org> Mon, 24 Apr 2017 12:44:23 -0400
|
||||
|
||||
|
|
14
debian/control
vendored
14
debian/control
vendored
|
@ -1,7 +1,7 @@
|
|||
Source: moulinette
|
||||
Section: python
|
||||
Priority: optional
|
||||
Maintainer: Jérôme Lebleu <jerome.lebleu@mailoo.org>
|
||||
Maintainer: YunoHost Contributors <contrib@yunohost.org>
|
||||
Build-Depends: debhelper (>= 9), python (>= 2.7), dh-python, python-setuptools, python-psutil, python-all (>= 2.7)
|
||||
Standards-Version: 3.9.6
|
||||
X-Python-Version: >= 2.7
|
||||
|
@ -18,12 +18,10 @@ Depends: ${misc:Depends}, ${python:Depends},
|
|||
python-toml,
|
||||
python-psutil,
|
||||
python-tz
|
||||
Replaces: yunohost-cli
|
||||
Breaks: yunohost-cli
|
||||
Breaks: yunohost (<< 4.1)
|
||||
Description: prototype interfaces with ease in Python
|
||||
The moulinette is a Python package that allows one to quickly and
|
||||
easily prototype interfaces for your application. Each action can
|
||||
be served through an HTTP API and from the command-line with a single
|
||||
method.
|
||||
Quickly and easily prototype interfaces for your application.
|
||||
Each action can be served through an HTTP API and from the
|
||||
command-line with a single method.
|
||||
.
|
||||
It was originally written for the YunoHost project.
|
||||
Originally designed and written for the YunoHost project.
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
"invalid_password": "كلمة السر خاطئة",
|
||||
"invalid_usage": "إستعمال غير صالح، إستخدم --help لعرض المساعدة",
|
||||
"ldap_attribute_already_exists": "الخاصية '{attribute}' موجودة مسبقا و تحمل القيمة '{value}'",
|
||||
"ldap_operation_error": "طرأ هناك خطأ أثناء عملية في LDAP",
|
||||
"ldap_server_down": "لا يمكن الإتصال بخادم LDAP",
|
||||
"logged_in": "مُتّصل",
|
||||
"logged_out": "تم تسجيل خروجك",
|
||||
|
@ -53,6 +52,9 @@
|
|||
"corrupted_yaml": "قراءة مُشوّهة لنسق yaml مِن {ressource:s} (السبب : {error:s})",
|
||||
"info": "معلومة:",
|
||||
"warn_the_user_about_waiting_lock_again": "جارٍ الانتظار…",
|
||||
"warn_the_user_that_lock_is_acquired": "لقد انتهى تنفيذ ذاك الأمر ، جارٍ إطلاق الأمر",
|
||||
"warn_the_user_about_waiting_lock": "هناك أمر لـ YunoHost قيد التشغيل حاليا. في انتظار انتهاء تنفيذه قبل تشغيل التالي"
|
||||
"warn_the_user_that_lock_is_acquired": "لقد انتهى تنفيذ ذاك الأمر للتوّ ، جارٍ تنفيذ هذا الأمر",
|
||||
"warn_the_user_about_waiting_lock": "هناك أمر لـ YunoHost قيد التشغيل حاليا. في انتظار انتهاء تنفيذه قبل تشغيل التالي",
|
||||
"ldap_server_is_down_restart_it": "إنّ خدمة LDAP غير مشغّلة ، نحن بصدد محاولة إعادة تشغيلها…",
|
||||
"session_expired": "لقد انتهت مدة صلاحية الجلسة. رجاءً أعد الإستيثاق.",
|
||||
"invalid_token": "إنّ الرمز المميز غير صالح - يرجى الإستيثاق"
|
||||
}
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
"invalid_password": "Contrasenya invàlida",
|
||||
"invalid_usage": "Utilització invàlida, utilitzeu --help per veure l'ajuda",
|
||||
"ldap_attribute_already_exists": "L'atribut '{attribute}' ja existeix amb el valor '{value}'",
|
||||
"ldap_operation_error": "Hi ha hagut un error durant l'operació de LDAP",
|
||||
"ldap_server_down": "No s'ha pogut connectar amb el servidor LDAP",
|
||||
"logged_in": "Sessió iniciada",
|
||||
"logged_out": "Sessió tancada",
|
||||
|
@ -40,21 +39,23 @@
|
|||
"cannot_open_file": "No s'ha pogut obrir el fitxer {file:s} (motiu: {error:s})",
|
||||
"cannot_write_file": "No s'ha pogut escriure el fitxer {file:s} (motiu: {error:s})",
|
||||
"unknown_error_reading_file": "Error desconegut al intentar llegir el fitxer {file:s} (motiu: {error:s})",
|
||||
"corrupted_json": "Json corrupte llegit des de {ressource:s} (motiu: {error:s})",
|
||||
"corrupted_yaml": "Yaml corrupte llegit des de {ressource:s} (motiu: {error:s})",
|
||||
"corrupted_json": "JSON corrupte llegit des de {ressource:s} (motiu: {error:s})",
|
||||
"corrupted_yaml": "YAML corrupte llegit des de {ressource:s} (motiu: {error:s})",
|
||||
"error_writing_file": "Error al escriure el fitxer {file:s}: {error:s}",
|
||||
"error_removing": "Error al eliminar {path:s}: {error:s}",
|
||||
"error_changing_file_permissions": "Error al canviar els permisos per {path:s}: {error:s}",
|
||||
"invalid_url": "Url invàlid {url:s} (el lloc web existeix?)",
|
||||
"invalid_url": "URL invàlid {url:s} (el lloc web existeix?)",
|
||||
"download_ssl_error": "Error SSL al connectar amb {url:s}",
|
||||
"download_timeout": "{url:s} ha tardat massa en respondre, s'ha deixat d'esperar.",
|
||||
"download_unknown_error": "Error al baixar dades des de {url:s}: {error:s}",
|
||||
"download_bad_status_code": "{url:s} ha retornat el codi d'estat {code:s}",
|
||||
"command_unknown": "Ordre '{command:s}' desconegut ?",
|
||||
"command_unknown": "Ordre '{command:s}' desconegut?",
|
||||
"info": "Info:",
|
||||
"corrupted_toml": "El fitxer TOML ha estat corromput en la lectura des de {ressource:s} (motiu: {error:s})",
|
||||
"warn_the_user_about_waiting_lock": "Hi ha una altra ordre de YunoHost en execució, s'executarà aquesta ordre un cop l'anterior hagi acabat",
|
||||
"warn_the_user_about_waiting_lock_again": "Encara en espera…",
|
||||
"warn_the_user_that_lock_is_acquired": "l'altra ordre tot just ha acabat, ara s'executarà aquesta ordre",
|
||||
"invalid_token": "Testimoni no vàlid - torneu-vos a autenticar"
|
||||
"warn_the_user_that_lock_is_acquired": "L'altra ordre tot just ha acabat, ara s'executarà aquesta ordre",
|
||||
"invalid_token": "Testimoni no vàlid - torneu-vos a autenticar",
|
||||
"ldap_server_is_down_restart_it": "El servei LDAP està caigut, s'està intentant tornar-lo a engegar…",
|
||||
"session_expired": "La sessió a expirat. Torneu-vos a autenticar."
|
||||
}
|
||||
|
|
|
@ -14,14 +14,13 @@
|
|||
"folder_exists": "目录已存在:{path}",
|
||||
"folder_not_exist": "目录不存在",
|
||||
"info": "信息:",
|
||||
"instance_already_running": "实例已正在运行",
|
||||
"instance_already_running": "已经有一个YunoHost操作正在运行。 请等待它完成再运行另一个。",
|
||||
"invalid_argument": "参数错误{argument}:{error}",
|
||||
"invalid_password": "密码错误",
|
||||
"invalid_usage": "用法错误,输入 --help 查看帮助信息",
|
||||
"ldap_attribute_already_exists": "参数{attribute}已赋值{value}",
|
||||
"ldap_operation_error": "LDAP操作时发生了错误",
|
||||
"ldap_server_down": "无法连接LDAP服务器",
|
||||
"logged_in": "登录成功",
|
||||
"logged_in": "登录",
|
||||
"logged_out": "登出",
|
||||
"not_logged_in": "您未登录",
|
||||
"operation_interrupted": "操作中断",
|
||||
|
@ -31,7 +30,7 @@
|
|||
"server_already_running": "服务已运行在指定端口",
|
||||
"success": "成功!",
|
||||
"unable_authenticate": "认证失败",
|
||||
"unable_retrieve_session": "获取会话失败",
|
||||
"unable_retrieve_session": "由于“ {exception}”,无法检索会话",
|
||||
"unknown_group": "未知组{group}",
|
||||
"unknown_user": "未知用户{user}",
|
||||
"values_mismatch": "值不匹配",
|
||||
|
@ -39,16 +38,23 @@
|
|||
"websocket_request_expected": "期望一个WebSocket请求",
|
||||
"cannot_open_file": "不能打开文件{file:s}(原因:{error:s})",
|
||||
"cannot_write_file": "写入文件{file:s}失败(原因:{error:s})",
|
||||
"unknown_error_reading_file": "尝试读取文件{file:s}时发生错误",
|
||||
"corrupted_json": "json数据{ressource:s}读取失败(原因:{error:s})",
|
||||
"corrupted_yaml": "读取yaml文件{ressource:s}失败(原因:{error:s})",
|
||||
"unknown_error_reading_file": "尝试读取文件{files}时发生未知错误(原因:{errors})",
|
||||
"corrupted_json": "从{ressource:s}读取的JSON损坏(原因:{error:s})",
|
||||
"corrupted_yaml": "从{ressource:s}读取的YMAL损坏(原因:{error:s})",
|
||||
"error_writing_file": "写入文件{file:s}失败:{error:s}",
|
||||
"error_removing": "删除路径{path:s}失败:{error:s}",
|
||||
"error_changing_file_permissions": "目录{path:s}权限修改失败:{error:s}",
|
||||
"invalid_url": "url:{url:s}无效(site是否存在?)",
|
||||
"invalid_url": "URL:{url:s}无效(site是否存在?)",
|
||||
"download_ssl_error": "连接{url:s}时发生SSL错误",
|
||||
"download_timeout": "{url:s}响应超时,放弃。",
|
||||
"download_unknown_error": "下载{url:s}失败:{error:s}",
|
||||
"download_bad_status_code": "{url:s}返回状态码:{code:s}",
|
||||
"command_unknown": "未知命令:{command:s}?"
|
||||
"command_unknown": "命令'{command:s}'未知?",
|
||||
"warn_the_user_that_lock_is_acquired": "另一个命令刚刚完成,现在启动此命令",
|
||||
"warn_the_user_about_waiting_lock_again": "还在等...",
|
||||
"warn_the_user_about_waiting_lock": "目前正在运行另一个YunoHost命令,我们在运行此命令之前等待它完成",
|
||||
"corrupted_toml": "从{ressource:s}读取的TOML损坏(原因:{error:s})",
|
||||
"invalid_token": "令牌无效-请进行身份验证",
|
||||
"ldap_server_is_down_restart_it": "LDAP服务已下线,正在尝试重启服务……",
|
||||
"session_expired": "会话已过期。请重新进行身份验证。"
|
||||
}
|
||||
|
|
3
locales/cs.json
Normal file
3
locales/cs.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"password": "Heslo"
|
||||
}
|
|
@ -16,7 +16,6 @@
|
|||
"invalid_password": "Passwort falsch",
|
||||
"invalid_usage": "Falscher Aufruf, verwende --help für den Hilfstext",
|
||||
"ldap_attribute_already_exists": "Attribute existieren bereits: '{attribute}={value}'",
|
||||
"ldap_operation_error": "Ein Fehler trat während der LDAP Abfrage auf",
|
||||
"ldap_server_down": "LDAP-Server nicht erreichbar",
|
||||
"logged_in": "Angemeldet",
|
||||
"logged_out": "Abgemeldet",
|
||||
|
@ -37,5 +36,26 @@
|
|||
"deprecated_command_alias": "'{prog} {old}' ist veraltet und wird bald entfernt werden, benutze '{prog} {new}' stattdessen",
|
||||
"unknown_group": "Gruppe '{group}' ist unbekannt",
|
||||
"unknown_user": "Benutzer '{user}' ist unbekannt",
|
||||
"info": "Info:"
|
||||
"info": "Info:",
|
||||
"invalid_token": "Ungültiger Token - bitte authentifizieren",
|
||||
"corrupted_json": "Beschädigtes JSON gelesen von {ressource:s} (reason: {error:s})",
|
||||
"unknown_error_reading_file": "Unbekannter Fehler beim Lesen der Datei {file:s} (reason: {error:s})",
|
||||
"cannot_write_file": "Kann Datei {file:s} nicht schreiben (reason: {error:s})",
|
||||
"cannot_open_file": "Kann Datei {file:s} nicht öffnen (reason: {error:s})",
|
||||
"corrupted_yaml": "Beschädigtes YAML gelesen von {ressource:s} (reason: {error:s})",
|
||||
"warn_the_user_that_lock_is_acquired": "Der andere Befehl wurde gerade abgeschlossen, starte jetzt diesen Befehl",
|
||||
"warn_the_user_about_waiting_lock_again": "Immer noch wartend...",
|
||||
"warn_the_user_about_waiting_lock": "Ein anderer YunoHost Befehl läuft gerade, wir warten bis er fertig ist, bevor dieser laufen kann",
|
||||
"command_unknown": "Befehl '{command:s}' unbekannt?",
|
||||
"download_bad_status_code": "{url:s} lieferte folgende(n) Status Code(s) {code:s}",
|
||||
"download_unknown_error": "Fehler beim Herunterladen von Daten von {url:s}: {error:s}",
|
||||
"download_timeout": "{url:s} brauchte zu lange zum Antworten, hab aufgegeben.",
|
||||
"download_ssl_error": "SSL Fehler beim Verbinden zu {url:s}",
|
||||
"invalid_url": "Ungültige URL {url:s} (existiert diese Seite?)",
|
||||
"error_changing_file_permissions": "Fehler beim Ändern der Berechtigungen für {path:s}: {error:s}",
|
||||
"error_removing": "Fehler beim Entfernen {path:s}: {error:s}",
|
||||
"error_writing_file": "Fehler beim Schreiben von Datei {file:s}: {error:s}",
|
||||
"corrupted_toml": "Beschädigtes TOML gelesen von {ressource:s} (reason: {error:s})",
|
||||
"ldap_server_is_down_restart_it": "Der LDAP-Dienst wurde angehalten. Es wird versucht, ihn erneut zu starten...",
|
||||
"session_expired": "Die Sitzung ist abgelaufen. Bitte neuauthentifizieren."
|
||||
}
|
||||
|
|
|
@ -19,7 +19,6 @@
|
|||
"invalid_token": "Invalid token - please authenticate",
|
||||
"invalid_usage": "Invalid usage, pass --help to see help",
|
||||
"ldap_attribute_already_exists": "Attribute '{attribute}' already exists with value '{value}'",
|
||||
"ldap_operation_error": "An error occurred during LDAP '{action}' operation",
|
||||
"ldap_server_down": "Unable to reach LDAP server",
|
||||
"logged_in": "Logged in",
|
||||
"logged_out": "Logged out",
|
||||
|
@ -32,6 +31,7 @@
|
|||
"success": "Success!",
|
||||
"unable_authenticate": "Unable to authenticate",
|
||||
"unable_retrieve_session": "Unable to retrieve the session because '{exception}'",
|
||||
"session_expired": "The session expired. Please re-authenticate.",
|
||||
"unknown_group": "Unknown '{group}' group",
|
||||
"unknown_user": "Unknown '{user}' user",
|
||||
"values_mismatch": "Values don't match",
|
||||
|
@ -41,19 +41,20 @@
|
|||
"cannot_open_file": "Could not open file {file:s} (reason: {error:s})",
|
||||
"cannot_write_file": "Could not write file {file:s} (reason: {error:s})",
|
||||
"unknown_error_reading_file": "Unknown error while trying to read file {file:s} (reason: {error:s})",
|
||||
"corrupted_json": "Corrupted json read from {ressource:s} (reason: {error:s})",
|
||||
"corrupted_yaml": "Corrupted yaml read from {ressource:s} (reason: {error:s})",
|
||||
"corrupted_toml": "Corrupted toml read from {ressource:s} (reason: {error:s})",
|
||||
"error_writing_file": "Error when writing file {file:s}: {error:s}",
|
||||
"corrupted_json": "Corrupted JSON read from {ressource:s} (reason: {error:s})",
|
||||
"corrupted_yaml": "Corrupted YAML read from {ressource:s} (reason: {error:s})",
|
||||
"corrupted_toml": "Corrupted TOML read from {ressource:s} (reason: {error:s})",
|
||||
"error_writing_file": "Error when writing file {file:s}: {error:s}",
|
||||
"error_removing": "Error when removing {path:s}: {error:s}",
|
||||
"error_changing_file_permissions": "Error when changing permissions for {path:s}: {error:s}",
|
||||
"invalid_url": "Invalid url {url:s} (does this site exists?)",
|
||||
"invalid_url": "Invalid URL {url:s} (does this site exists?)",
|
||||
"download_ssl_error": "SSL error when connecting to {url:s}",
|
||||
"download_timeout": "{url:s} took too long to answer, gave up.",
|
||||
"download_unknown_error": "Error when downloading data from {url:s}: {error:s}",
|
||||
"download_bad_status_code": "{url:s} returned status code {code:s}",
|
||||
"command_unknown": "Command '{command:s}' unknown ?",
|
||||
"command_unknown": "Command '{command:s}' unknown?",
|
||||
"warn_the_user_about_waiting_lock": "Another YunoHost command is running right now, we are waiting for it to finish before running this one",
|
||||
"warn_the_user_about_waiting_lock_again": "Still waiting...",
|
||||
"warn_the_user_that_lock_is_acquired": "the other command just completed, now starting this command"
|
||||
"warn_the_user_that_lock_is_acquired": "The other command just completed, now starting this command",
|
||||
"ldap_server_is_down_restart_it": "The LDAP service is down, attempt to restart it..."
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"password": "Pasvorto",
|
||||
"colon": "{}: ",
|
||||
"warn_the_user_that_lock_is_acquired": "la alia komando nur kompletigas, nun komencante ĉi tiun komandon",
|
||||
"warn_the_user_that_lock_is_acquired": "la alia komando ĵus kompletigis, nun komencante ĉi tiun komandon",
|
||||
"warn_the_user_about_waiting_lock_again": "Ankoraŭ atendanta...",
|
||||
"warn_the_user_about_waiting_lock": "Alia komando de YunoHost funkcias ĝuste nun, ni atendas, ke ĝi finiĝos antaŭ ol funkcii ĉi tiu",
|
||||
"command_unknown": "Komando '{command:s}' nekonata?",
|
||||
|
@ -34,7 +34,6 @@
|
|||
"not_logged_in": "Vi ne estas ensalutinta",
|
||||
"logged_in": "Ensalutinta",
|
||||
"ldap_server_down": "Ne eblas atingi la servilon LDAP",
|
||||
"ldap_operation_error": "Eraro okazis dum LDAP-operacio",
|
||||
"ldap_attribute_already_exists": "Atributo '{attribute}' jam ekzistas kun valoro '{value}'",
|
||||
"invalid_usage": "Nevalida uzado, preterpase '--help' por vidi helpon",
|
||||
"invalid_password": "Nevalida pasvorto",
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
"invalid_password": "Contraseña no válida",
|
||||
"invalid_usage": "Uso no válido, utilice --help para ver la ayuda",
|
||||
"ldap_attribute_already_exists": "El atributo «{attribute}» ya existe con el valor «{value}»",
|
||||
"ldap_operation_error": "Ha ocurrido un error durante la operación de LDAP",
|
||||
"ldap_server_down": "No se pudo conectar con el servidor LDAP",
|
||||
"logged_in": "Sesión iniciada",
|
||||
"logged_out": "Sesión cerrada",
|
||||
|
@ -40,21 +39,23 @@
|
|||
"cannot_open_file": "No se pudo abrir el archivo {file:s} (motivo: {error:s})",
|
||||
"cannot_write_file": "No se pudo escribir el archivo {file:s} (motivo: {error:s})",
|
||||
"unknown_error_reading_file": "Error desconocido al intentar leer el archivo {file:s} (motivo: {error:s})",
|
||||
"corrupted_json": "Lectura corrupta de Json desde {ressource:s} (motivo: {error:s})",
|
||||
"corrupted_json": "Lectura corrupta de JSON desde {ressource:s} (motivo: {error:s})",
|
||||
"error_writing_file": "Error al escribir el archivo {file:s}: {error:s}",
|
||||
"error_removing": "Error al eliminar {path:s}: {error:s}",
|
||||
"error_changing_file_permissions": "Error al cambiar los permisos para {path:s}: {error:s}",
|
||||
"invalid_url": "Url no válida {url:s} (¿Existe este sitio?)",
|
||||
"invalid_url": "URL inválida {url:s} (¿Existe este sitio?)",
|
||||
"download_ssl_error": "Error SSL al conectar con {url:s}",
|
||||
"download_timeout": "{url:s} tardó demasiado en responder, abandono.",
|
||||
"download_unknown_error": "Error al descargar datos desde {url:s} : {error:s}",
|
||||
"download_bad_status_code": "{url:s} devolvió el código de estado {code:s}",
|
||||
"command_unknown": "¿Orden «{command:s}» desconocida?",
|
||||
"corrupted_yaml": "Lectura corrupta de yaml desde {ressource:s} (motivo: {error:s})",
|
||||
"corrupted_yaml": "Lectura corrupta de YAML desde {ressource:s} (motivo: {error:s})",
|
||||
"info": "Información:",
|
||||
"corrupted_toml": "Lectura corrupta de TOML desde {ressource:s} (motivo: {error:s})",
|
||||
"warn_the_user_that_lock_is_acquired": "la otra orden recién terminó, iniciando esta orden ahora",
|
||||
"warn_the_user_that_lock_is_acquired": "La otra orden recién terminó, iniciando esta orden ahora",
|
||||
"warn_the_user_about_waiting_lock_again": "Aún esperando...",
|
||||
"warn_the_user_about_waiting_lock": "Otra orden de YunoHost se está ejecutando ahora, estamos esperando a que termine antes de ejecutar esta",
|
||||
"invalid_token": "Token invalido - vuelva a autenticarte"
|
||||
"invalid_token": "Token invalido - vuelva a autenticarte",
|
||||
"ldap_server_is_down_restart_it": "El servicio LDAP está caído, intentando reiniciarlo...",
|
||||
"session_expired": "La sesión expiró. Por favor autenticarse de nuevo."
|
||||
}
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
"invalid_password": "Mot de passe incorrect",
|
||||
"invalid_usage": "Utilisation erronée, utilisez --help pour accéder à l’aide",
|
||||
"ldap_attribute_already_exists": "L’attribut '{attribute}' existe déjà avec la valeur suivante : '{value}'",
|
||||
"ldap_operation_error": "Une erreur est survenue lors de l’opération LDAP",
|
||||
"ldap_server_down": "Impossible d’atteindre le serveur LDAP",
|
||||
"logged_in": "Connecté",
|
||||
"logged_out": "Déconnecté",
|
||||
|
@ -55,6 +54,8 @@
|
|||
"corrupted_toml": "Fichier TOML corrompu en lecture depuis {ressource:s} (cause : {error:s})",
|
||||
"warn_the_user_about_waiting_lock": "Une autre commande YunoHost est actuellement en cours, nous attendons qu'elle se termine avant de démarrer celle là",
|
||||
"warn_the_user_about_waiting_lock_again": "Toujours en attente...",
|
||||
"warn_the_user_that_lock_is_acquired": "l'autre commande vient de se terminer, lancement de cette commande",
|
||||
"invalid_token": "Jeton non valide - veuillez vous authentifier"
|
||||
"warn_the_user_that_lock_is_acquired": "La commande précédente vient de se terminer, lancement de cette nouvelle commande",
|
||||
"invalid_token": "Jeton non valide - veuillez vous authentifier",
|
||||
"ldap_server_is_down_restart_it": "Le service LDAP est arrêté, nous tentons de le redémarrer...",
|
||||
"session_expired": "La session a expiré. Merci de vous ré-authentifier."
|
||||
}
|
||||
|
|
|
@ -18,7 +18,6 @@
|
|||
"invalid_password": "अवैध पासवर्ड",
|
||||
"invalid_usage": "अवैध उपयोग, सहायता देखने के लिए --help साथ लिखे।",
|
||||
"ldap_attribute_already_exists": "'{attribute}' तर्क पहले इस वैल्यू '{value}' से मौजूद है।",
|
||||
"ldap_operation_error": "LDAP ऑपरेशन के दौरान त्रुटि हो गई है।",
|
||||
"ldap_server_down": "LDAP सर्वर तक पहुंचने में असमर्थ।",
|
||||
"logged_in": "लोग्ड इन",
|
||||
"logged_out": "लॉग आउट",
|
||||
|
|
|
@ -20,7 +20,6 @@
|
|||
"invalid_password": "Password non valida",
|
||||
"invalid_usage": "Utilizzo non valido, usa --help per vedere l'aiuto",
|
||||
"ldap_attribute_already_exists": "L'attributo '{attribute}' esiste già con valore '{value}'",
|
||||
"ldap_operation_error": "Si è verificato un errore durante l'operazione LDAP",
|
||||
"ldap_server_down": "Impossibile raggiungere il server LDAP",
|
||||
"logged_in": "Connesso",
|
||||
"not_logged_in": "Non hai effettuato l'accesso",
|
||||
|
@ -40,21 +39,23 @@
|
|||
"cannot_open_file": "Impossibile aprire il file {file:s} (motivo: {error:s})",
|
||||
"cannot_write_file": "Impossibile scrivere il file {file:s} (motivo: {error:s})",
|
||||
"unknown_error_reading_file": "Errore sconosciuto durante il tentativo di leggere il file {file:s} (motivo: {errore:s})",
|
||||
"corrupted_json": "Lettura json corrotta da {ressource:s} (motivo: {error:s})",
|
||||
"corrupted_yaml": "Lettura yaml corrotta da {ressource:s} (motivo: {error:s})",
|
||||
"corrupted_json": "Lettura JSON corrotta da {resource:s} (motivo: {error:s})",
|
||||
"corrupted_yaml": "Lettura YAML corrotta da {resource:s} (motivo: {error:s})",
|
||||
"error_writing_file": "Errore durante la scrittura del file {file:s}: {error:s}",
|
||||
"error_removing": "Errore durante la rimozione {path:s}: {error:s}",
|
||||
"error_changing_file_permissions": "Errore durante il cambio di permessi per {path:s}: {error:s}",
|
||||
"invalid_url": "URL non valido {url:s} (questo sito esiste?)",
|
||||
"invalid_url": "URL non valido {url:s} (il sito esiste?)",
|
||||
"download_ssl_error": "Errore SSL durante la connessione a {url:s}",
|
||||
"download_timeout": "{url:s} ci ha messo troppo a rispondere, abbandonato.",
|
||||
"download_unknown_error": "Errore durante il download di dati da {url:s} : {error:s}",
|
||||
"download_bad_status_code": "{url:s} ha restituito il codice di stato {code:s}",
|
||||
"command_unknown": "Comando '{command:s}' sconosciuto ?",
|
||||
"command_unknown": "Comando '{command:s}' sconosciuto?",
|
||||
"info": "Info:",
|
||||
"warn_the_user_that_lock_is_acquired": "l'altro comando è appena completato, ora avvia questo comando",
|
||||
"warn_the_user_that_lock_is_acquired": "L'altro comando è appena completato, ora avvio questo comando",
|
||||
"warn_the_user_about_waiting_lock_again": "Sto ancora aspettando ...",
|
||||
"warn_the_user_about_waiting_lock": "Un altro comando YunoHost è in esecuzione in questo momento, stiamo aspettando che finisca prima di eseguire questo",
|
||||
"corrupted_toml": "Toml corrotto da {ressource:s} (motivo: {errore:s})",
|
||||
"invalid_token": "Token non valido: autenticare"
|
||||
"corrupted_toml": "TOML corrotto da {ressource:s} (motivo: {errore:s})",
|
||||
"invalid_token": "Token non valido: autenticare",
|
||||
"session_expired": "La sessione è terminata. Sei pregato di autenticarti nuovamente.",
|
||||
"ldap_server_is_down_restart_it": "Il servizio LDAP è terminato, provo a riavviarlo..."
|
||||
}
|
||||
|
|
|
@ -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}' आवश्यक छ"
|
||||
}
|
||||
|
|
|
@ -16,7 +16,6 @@
|
|||
"invalid_password": "Ongeldig wachtwoord",
|
||||
"invalid_usage": "Ongeldig gebruik, doe --help om de hulptekst te lezen",
|
||||
"ldap_attribute_already_exists": "Attribuut '{attribute}' bestaat al met waarde '{value}'",
|
||||
"ldap_operation_error": "Er is een fout opgetreden bij het uitvoeren van LDAP operatie",
|
||||
"ldap_server_down": "Kan LDAP server niet bereiken",
|
||||
"logged_in": "Ingelogd",
|
||||
"logged_out": "Uitgelogd",
|
||||
|
@ -50,7 +49,7 @@
|
|||
"download_unknown_error": "Fout tijdens het downloaden van data van {url:s}: {error:s}",
|
||||
"download_bad_status_code": "{url:s} stuurt status code {code:s}",
|
||||
"command_unknown": "Opdracht '{command:s}' ongekend ?",
|
||||
"warn_the_user_that_lock_is_acquired": "het andere commando is net voltooid, starten van dit commando",
|
||||
"warn_the_user_that_lock_is_acquired": "de andere opdracht is zojuist voltooid en start nu deze opdracht",
|
||||
"warn_the_user_about_waiting_lock_again": "Nog steeds aan het wachten...",
|
||||
"warn_the_user_about_waiting_lock": "Een ander YunoHost commando wordt uitgevoerd, we wachten tot het gedaan is alovrens dit te starten",
|
||||
"corrupted_toml": "Ongeldige TOML werd gelezen op {ressource:s} (reason: {error:s})",
|
||||
|
|
|
@ -31,7 +31,6 @@
|
|||
"warning": "Atencion :",
|
||||
"invalid_usage": "Usatge invalid, utilizatz --help per accedir a l’ajuda",
|
||||
"ldap_attribute_already_exists": "L’atribut « {attribute} » existís ja amb la valor : {value}",
|
||||
"ldap_operation_error": "Una error s’es producha pendent l’operacion LDAP",
|
||||
"operation_interrupted": "Operacion interrompuda",
|
||||
"server_already_running": "Un servidor es ja en execucion sus aqueste pòrt",
|
||||
"success": "Capitada !",
|
||||
|
@ -56,5 +55,7 @@
|
|||
"warn_the_user_about_waiting_lock": "Una autra comanda YunoHost es en execucion, sèm a esperar qu’acabe abans d’aviar aquesta d’aquí",
|
||||
"warn_the_user_about_waiting_lock_again": "Encara en espèra…",
|
||||
"warn_the_user_that_lock_is_acquired": "l’autra comanda ven d’acabar, ara lançament d’aquesta comanda",
|
||||
"invalid_token": "Geton invalid - volgatz vos autentificar"
|
||||
"invalid_token": "Geton invalid - volgatz vos autentificar",
|
||||
"ldap_server_is_down_restart_it": "Lo servici LDAP s’es atudat, ensajam de lo reaviar…",
|
||||
"session_expired": "La session a expirat. Tornatz vos autentificar."
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"logged_out": "Wylogowano",
|
||||
"password": "hasło",
|
||||
"warn_the_user_that_lock_is_acquired": "drugie polecenie właśnie się zakończyło, teraz uruchamia to polecenie",
|
||||
"password": "Hasło",
|
||||
"warn_the_user_that_lock_is_acquired": "drugie polecenie właśnie się zakończyło, teraz uruchamiając to polecenie",
|
||||
"warn_the_user_about_waiting_lock_again": "Wciąż czekam...",
|
||||
"warn_the_user_about_waiting_lock": "Kolejne polecenie YunoHost jest teraz uruchomione, czekamy na jego zakończenie przed uruchomieniem tego",
|
||||
"command_unknown": "Polecenie „{command:s}” jest nieznane?",
|
||||
|
@ -34,7 +34,6 @@
|
|||
"not_logged_in": "Nie jesteś zalogowany",
|
||||
"logged_in": "Zalogowany",
|
||||
"ldap_server_down": "Nie można połączyć się z serwerem LDAP",
|
||||
"ldap_operation_error": "Wystąpił błąd podczas operacji LDAP",
|
||||
"ldap_attribute_already_exists": "Atrybut „{attribute}” już istnieje z wartością „{value}”",
|
||||
"invalid_usage": "Nieprawidłowe użycie. Przejdź --help, aby wyświetlić pomoc",
|
||||
"invalid_token": "Nieprawidłowy token - proszę uwierzytelnić",
|
||||
|
|
|
@ -15,7 +15,6 @@
|
|||
"invalid_password": "Senha incorreta",
|
||||
"invalid_usage": "Uso invalido, utilizar --help para ver a ajuda",
|
||||
"ldap_attribute_already_exists": "O atributo '{attribute}' já existe com valor '{value}'",
|
||||
"ldap_operation_error": "Um erro ocorreu durante a operação LDAP",
|
||||
"ldap_server_down": "Não foi possível comunicar com o servidor LDAP",
|
||||
"logged_in": "Sessão iniciada",
|
||||
"logged_out": "Sessão terminada",
|
||||
|
@ -43,18 +42,20 @@
|
|||
"error_writing_file": "Erro ao gravar arquivo {file:s}: {error:s}",
|
||||
"error_removing": "Erro ao remover {path:s}: {error:s}",
|
||||
"error_changing_file_permissions": "Erro ao alterar as permissões para {path:s}: {error:s}",
|
||||
"invalid_url": "URL inválida {url:s} (does this site exists ?)",
|
||||
"invalid_url": "URL inválida {url:s} (Esse site existe ?)",
|
||||
"download_ssl_error": "Erro de SSL ao conectar-se a {url:s}",
|
||||
"download_timeout": "{url:s} demorou muito para responder, desistiu.",
|
||||
"download_unknown_error": "Erro quando baixando os dados de {url:s} : {error:s}",
|
||||
"download_bad_status_code": "{url:s} retornou o código de status {code:s}",
|
||||
"command_unknown": "Comando '{command:s}' desconhecido ?",
|
||||
"corrupted_json": "Json corrompido lido do {ressource:s} (motivo: {error:s})",
|
||||
"corrupted_yaml": "Yaml corrompido lido do {ressource:s} (motivo: {error:s})",
|
||||
"warn_the_user_that_lock_is_acquired": "o outro comando acabou de concluir, agora iniciando este comando",
|
||||
"corrupted_json": "JSON corrompido lido do {ressource:s} (motivo: {error:s})",
|
||||
"corrupted_yaml": "YAML corrompido lido do {ressource:s} (motivo: {error:s})",
|
||||
"warn_the_user_that_lock_is_acquired": "O outro comando acabou de concluir, agora iniciando este comando",
|
||||
"warn_the_user_about_waiting_lock_again": "Ainda esperando...",
|
||||
"warn_the_user_about_waiting_lock": "Outro comando YunoHost está sendo executado agora, estamos aguardando o término antes de executar este",
|
||||
"corrupted_toml": "Toml corrompido lido em {ressource:s} (motivo: {error:s})",
|
||||
"corrupted_toml": "TOML corrompido lido em {ressource:s} (motivo: {error:s})",
|
||||
"invalid_token": "Token inválido - autentique",
|
||||
"info": "Informações:"
|
||||
"info": "Informações:",
|
||||
"ldap_server_is_down_restart_it": "O serviço LDAP esta caído, tentando reiniciá-lo...",
|
||||
"session_expired": "A sessão expirou. Se autentique de novo por favor."
|
||||
}
|
||||
|
|
|
@ -41,7 +41,6 @@
|
|||
"download_timeout": "Превышено время ожидания ответа от {url:s}.",
|
||||
"download_unknown_error": "Ошибка при загрузке данных с {url:s} : {error:s}",
|
||||
"instance_already_running": "Операция YunoHost уже запущена. Пожалуйста, подождите, пока он закончится, прежде чем запускать другой.",
|
||||
"ldap_operation_error": "Ошибка в процессе работы LDAP",
|
||||
"root_required": "Чтобы выполнить это действие, вы должны иметь права root",
|
||||
"corrupted_json": "Повреждённый json получен от {ressource:s} (причина: {error:s})",
|
||||
"command_unknown": "Команда '{command:s}' неизвестна ?",
|
||||
|
|
|
@ -49,7 +49,6 @@
|
|||
"unable_retrieve_session": "Det gick inte att hämta sessionen eftersom '{exception}'",
|
||||
"unable_authenticate": "Det går inte att verifiera",
|
||||
"ldap_server_down": "Det går inte att nå LDAP-servern",
|
||||
"ldap_operation_error": "Ett fel inträffade under LDAP-drift",
|
||||
"invalid_usage": "Ogiltig användning, pass --help för att se hjälp",
|
||||
"invalid_token": "Ogiltigt token - verifiera",
|
||||
"instance_already_running": "Det finns redan en YunoHost-operation. Vänta tills den är klar innan du kör en annan.",
|
||||
|
|
|
@ -11,7 +11,6 @@
|
|||
"invalid_argument": "Geçersiz argüman '{argument}': {error}",
|
||||
"invalid_password": "Geçersiz parola",
|
||||
"ldap_attribute_already_exists": "'{attribute}={value}' özelliği zaten mevcut",
|
||||
"ldap_operation_error": "LDAP işlemi sırasında hata oluştu",
|
||||
"ldap_server_down": "LDAP sunucusuna erişilemiyor",
|
||||
"logged_in": "Giriş yapıldı",
|
||||
"logged_out": "Çıkış yapıldı",
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from moulinette.core import (
|
||||
init_interface,
|
||||
MoulinetteError,
|
||||
MoulinetteSignals,
|
||||
Moulinette18n,
|
||||
|
@ -9,8 +8,7 @@ from moulinette.core import (
|
|||
from moulinette.globals import init_moulinette_env
|
||||
|
||||
__title__ = "moulinette"
|
||||
__version__ = "0.1"
|
||||
__author__ = ["Kload", "jlebleu", "titoko", "beudbeud", "npze"]
|
||||
__author__ = ["Yunohost Contributors"]
|
||||
__license__ = "AGPL 3.0"
|
||||
__credits__ = """
|
||||
Copyright (C) 2014 YUNOHOST.ORG
|
||||
|
@ -73,85 +71,57 @@ def init(logging_config=None, **kwargs):
|
|||
|
||||
|
||||
# Easy access to interfaces
|
||||
|
||||
|
||||
def api(
|
||||
namespaces, host="localhost", port=80, routes={}, use_websocket=True, use_cache=True
|
||||
):
|
||||
def api(host="localhost", port=80, routes={}):
|
||||
"""Web server (API) interface
|
||||
|
||||
Run a HTTP server with the moulinette for an API usage.
|
||||
|
||||
Keyword arguments:
|
||||
- namespaces -- The list of namespaces to use
|
||||
- host -- Server address to bind to
|
||||
- port -- Server port to bind to
|
||||
- routes -- A dict of additional routes to add in the form of
|
||||
{(method, uri): callback}
|
||||
- use_websocket -- Serve via WSGI to handle asynchronous responses
|
||||
- use_cache -- False if it should parse the actions map file
|
||||
instead of using the cached one
|
||||
|
||||
"""
|
||||
from moulinette.interfaces.api import Interface as Api
|
||||
|
||||
try:
|
||||
moulinette = init_interface(
|
||||
"api",
|
||||
kwargs={"routes": routes, "use_websocket": use_websocket},
|
||||
actionsmap={"namespaces": namespaces, "use_cache": use_cache},
|
||||
)
|
||||
moulinette.run(host, port)
|
||||
Api(routes=routes).run(host, port)
|
||||
except MoulinetteError as e:
|
||||
import logging
|
||||
|
||||
logging.getLogger(namespaces[0]).error(e.strerror)
|
||||
return e.errno if hasattr(e, "errno") else 1
|
||||
logging.getLogger(logging.main_logger).error(e.strerror)
|
||||
return 1
|
||||
except KeyboardInterrupt:
|
||||
import logging
|
||||
|
||||
logging.getLogger(namespaces[0]).info(m18n.g("operation_interrupted"))
|
||||
logging.getLogger(logging.main_logger).info(m18n.g("operation_interrupted"))
|
||||
return 0
|
||||
|
||||
|
||||
def cli(
|
||||
namespaces,
|
||||
args,
|
||||
use_cache=True,
|
||||
output_as=None,
|
||||
password=None,
|
||||
timeout=None,
|
||||
parser_kwargs={},
|
||||
):
|
||||
def cli(args, top_parser, output_as=None, timeout=None):
|
||||
"""Command line interface
|
||||
|
||||
Execute an action with the moulinette from the CLI and print its
|
||||
result in a readable format.
|
||||
|
||||
Keyword arguments:
|
||||
- namespaces -- The list of namespaces to use
|
||||
- args -- A list of argument strings
|
||||
- use_cache -- False if it should parse the actions map file
|
||||
instead of using the cached one
|
||||
- output_as -- Output result in another format, see
|
||||
moulinette.interfaces.cli.Interface for possible values
|
||||
- password -- The password to use in case of authentication
|
||||
- parser_kwargs -- A dict of arguments to pass to the parser
|
||||
class at construction
|
||||
- top_parser -- The top parser used to build the ActionsMapParser
|
||||
|
||||
"""
|
||||
from moulinette.interfaces.cli import Interface as Cli
|
||||
|
||||
try:
|
||||
moulinette = init_interface(
|
||||
"cli",
|
||||
actionsmap={
|
||||
"namespaces": namespaces,
|
||||
"use_cache": use_cache,
|
||||
"parser_kwargs": parser_kwargs,
|
||||
},
|
||||
load_only_category = args[0] if args and not args[0].startswith("-") else None
|
||||
Cli(top_parser=top_parser, load_only_category=load_only_category).run(
|
||||
args, output_as=output_as, timeout=timeout
|
||||
)
|
||||
moulinette.run(args, output_as=output_as, password=password, timeout=timeout)
|
||||
except MoulinetteError as e:
|
||||
import logging
|
||||
|
||||
logging.getLogger(namespaces[0]).error(e.strerror)
|
||||
logging.getLogger(logging.main_logger).error(e.strerror)
|
||||
return 1
|
||||
return 0
|
||||
|
||||
|
|
|
@ -4,7 +4,28 @@ import os
|
|||
import re
|
||||
import logging
|
||||
import yaml
|
||||
import pickle as pickle
|
||||
import glob
|
||||
|
||||
import sys
|
||||
if sys.version_info[0] == 3:
|
||||
# python 3
|
||||
import pickle as pickle
|
||||
else:
|
||||
# python 2
|
||||
import cPickle as pickle
|
||||
import codecs
|
||||
import warnings
|
||||
def open(file, mode='r', buffering=-1, encoding=None,
|
||||
errors=None, newline=None, closefd=True, opener=None):
|
||||
if newline is not None:
|
||||
warnings.warn('newline is not supported in py2')
|
||||
if not closefd:
|
||||
warnings.warn('closefd is not supported in py2')
|
||||
if opener is not None:
|
||||
warnings.warn('opener is not supported in py2')
|
||||
return codecs.open(filename=file, mode=mode, encoding=encoding,
|
||||
errors=errors, buffering=buffering)
|
||||
|
||||
from time import time
|
||||
from collections import OrderedDict
|
||||
from importlib import import_module
|
||||
|
@ -282,7 +303,6 @@ class ExtraArgumentParser(object):
|
|||
if iface in klass.skipped_iface:
|
||||
continue
|
||||
self.extra[klass.name] = klass
|
||||
logger.debug("extra parameter classes loaded: %s", self.extra.keys())
|
||||
|
||||
def validate(self, arg_name, parameters):
|
||||
"""
|
||||
|
@ -294,7 +314,7 @@ class ExtraArgumentParser(object):
|
|||
|
||||
"""
|
||||
# Iterate over parameters to validate
|
||||
for p, v in parameters.items():
|
||||
for p in list(parameters):
|
||||
klass = self.extra.get(p, None)
|
||||
if not klass:
|
||||
# Remove unknown parameters
|
||||
|
@ -302,7 +322,7 @@ class ExtraArgumentParser(object):
|
|||
else:
|
||||
try:
|
||||
# Validate parameter value
|
||||
parameters[p] = klass.validate(v, arg_name)
|
||||
parameters[p] = klass.validate(parameters[p], arg_name)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
"unable to validate extra parameter '%s' "
|
||||
|
@ -398,36 +418,32 @@ class ActionsMap(object):
|
|||
Moreover, the action can have specific argument(s).
|
||||
|
||||
This class allows to manipulate one or several actions maps
|
||||
associated to a namespace. If no namespace is given, it will load
|
||||
all available namespaces.
|
||||
associated to a namespace.
|
||||
|
||||
Keyword arguments:
|
||||
- parser_class -- The BaseActionsMapParser derived class to use
|
||||
for parsing the actions map
|
||||
- namespaces -- The list of namespaces to use
|
||||
- use_cache -- False if it should parse the actions map file
|
||||
instead of using the cached one
|
||||
- parser_kwargs -- A dict of arguments to pass to the parser
|
||||
class at construction
|
||||
|
||||
- top_parser -- A BaseActionsMapParser-derived instance to use for
|
||||
parsing the actions map
|
||||
- load_only_category -- A name of a category that should only be the
|
||||
one loaded because it's been already determined
|
||||
that's the only one relevant ... used for optimization
|
||||
purposes...
|
||||
"""
|
||||
|
||||
def __init__(self, parser_class, namespaces=[], use_cache=True, parser_kwargs={}):
|
||||
if not issubclass(parser_class, BaseActionsMapParser):
|
||||
raise ValueError("Invalid parser class '%s'" % parser_class.__name__)
|
||||
self.parser_class = parser_class
|
||||
self.use_cache = use_cache
|
||||
def __init__(self, top_parser, load_only_category=None):
|
||||
|
||||
assert isinstance(top_parser, BaseActionsMapParser), (
|
||||
"Invalid parser class '%s'" % top_parser.__class__.__name__
|
||||
)
|
||||
|
||||
moulinette_env = init_moulinette_env()
|
||||
DATA_DIR = moulinette_env["DATA_DIR"]
|
||||
CACHE_DIR = moulinette_env["CACHE_DIR"]
|
||||
|
||||
if len(namespaces) == 0:
|
||||
namespaces = self.get_namespaces()
|
||||
actionsmaps = OrderedDict()
|
||||
|
||||
self.from_cache = False
|
||||
# Iterate over actions map namespaces
|
||||
for n in namespaces:
|
||||
for n in self.get_namespaces():
|
||||
logger.debug("loading actions map namespace '%s'", n)
|
||||
|
||||
actionsmap_yml = "%s/actionsmap/%s.yml" % (DATA_DIR, n)
|
||||
|
@ -439,33 +455,36 @@ class ActionsMap(object):
|
|||
actionsmap_yml_stat.st_mtime,
|
||||
)
|
||||
|
||||
if use_cache and os.path.exists(actionsmap_pkl):
|
||||
if os.path.exists(actionsmap_pkl):
|
||||
try:
|
||||
# Attempt to load cache
|
||||
with open(actionsmap_pkl, "rb") as f:
|
||||
actionsmaps[n] = pickle.load(f)
|
||||
|
||||
self.from_cache = True
|
||||
# TODO: Switch to python3 and catch proper exception
|
||||
except (IOError, EOFError):
|
||||
self.use_cache = False
|
||||
actionsmaps = self.generate_cache(namespaces)
|
||||
elif use_cache: # cached file doesn't exists
|
||||
self.use_cache = False
|
||||
actionsmaps = self.generate_cache(namespaces)
|
||||
elif n not in actionsmaps:
|
||||
with open(actionsmap_yml) as f:
|
||||
actionsmaps[n] = ordered_yaml_load(f)
|
||||
actionsmaps[n] = self.generate_cache(n)
|
||||
else: # cache file doesn't exists
|
||||
actionsmaps[n] = self.generate_cache(n)
|
||||
|
||||
# If load_only_category is set, and *if* the target category
|
||||
# is in the actionsmap, we'll load only that one.
|
||||
# If we filter it even if it doesn't exist, we'll end up with a
|
||||
# weird help message when we do a typo in the category name..
|
||||
if load_only_category and load_only_category in actionsmaps[n]:
|
||||
actionsmaps[n] = {
|
||||
k: v
|
||||
for k, v in actionsmaps[n].items()
|
||||
if k in [load_only_category, "_global"]
|
||||
}
|
||||
|
||||
# Load translations
|
||||
m18n.load_namespace(n)
|
||||
|
||||
# Generate parsers
|
||||
self.extraparser = ExtraArgumentParser(parser_class.interface)
|
||||
self._parser = self._construct_parser(actionsmaps, **parser_kwargs)
|
||||
|
||||
@property
|
||||
def parser(self):
|
||||
"""Return the instance of the interface's actions map parser"""
|
||||
return self._parser
|
||||
self.extraparser = ExtraArgumentParser(top_parser.interface)
|
||||
self.parser = self._construct_parser(actionsmaps, top_parser)
|
||||
|
||||
def get_authenticator_for_profile(self, auth_profile):
|
||||
|
||||
|
@ -563,6 +582,9 @@ class ActionsMap(object):
|
|||
)
|
||||
func = getattr(mod, func_name)
|
||||
except (AttributeError, ImportError):
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
logger.exception("unable to load function %s.%s", namespace, func_name)
|
||||
raise MoulinetteError("error_see_log")
|
||||
else:
|
||||
|
@ -601,85 +623,81 @@ class ActionsMap(object):
|
|||
moulinette_env = init_moulinette_env()
|
||||
DATA_DIR = moulinette_env["DATA_DIR"]
|
||||
|
||||
for f in os.listdir("%s/actionsmap" % DATA_DIR):
|
||||
if f.endswith(".yml"):
|
||||
namespaces.append(f[:-4])
|
||||
# This var is ['*'] by default but could be set for example to
|
||||
# ['yunohost', 'yml_*']
|
||||
NAMESPACE_PATTERNS = moulinette_env["NAMESPACES"]
|
||||
|
||||
# Look for all files that match the given patterns in the actionsmap dir
|
||||
for namespace_pattern in NAMESPACE_PATTERNS:
|
||||
namespaces.extend(
|
||||
glob.glob("%s/actionsmap/%s.yml" % (DATA_DIR, namespace_pattern))
|
||||
)
|
||||
|
||||
# Keep only the filenames with extension
|
||||
namespaces = [os.path.basename(n)[:-4] for n in namespaces]
|
||||
|
||||
return namespaces
|
||||
|
||||
@classmethod
|
||||
def generate_cache(klass, namespaces=None):
|
||||
def generate_cache(klass, namespace):
|
||||
"""
|
||||
Generate cache for the actions map's file(s)
|
||||
|
||||
Keyword arguments:
|
||||
- namespaces -- A list of namespaces to generate cache for
|
||||
- namespace -- The namespace to generate cache for
|
||||
|
||||
Returns:
|
||||
A dict of actions map for each namespaces
|
||||
|
||||
The action map for the namespace
|
||||
"""
|
||||
moulinette_env = init_moulinette_env()
|
||||
CACHE_DIR = moulinette_env["CACHE_DIR"]
|
||||
DATA_DIR = moulinette_env["DATA_DIR"]
|
||||
|
||||
actionsmaps = {}
|
||||
if not namespaces:
|
||||
namespaces = klass.get_namespaces()
|
||||
|
||||
# Iterate over actions map namespaces
|
||||
for n in namespaces:
|
||||
logger.debug("generating cache for actions map namespace '%s'", n)
|
||||
logger.debug("generating cache for actions map namespace '%s'", namespace)
|
||||
|
||||
# Read actions map from yaml file
|
||||
am_file = "%s/actionsmap/%s.yml" % (DATA_DIR, n)
|
||||
with open(am_file, "r") as f:
|
||||
actionsmaps[n] = ordered_yaml_load(f)
|
||||
# Read actions map from yaml file
|
||||
am_file = "%s/actionsmap/%s.yml" % (DATA_DIR, namespace)
|
||||
with open(am_file, "r") as f:
|
||||
actionsmap = ordered_yaml_load(f)
|
||||
|
||||
# at installation, cachedir might not exists
|
||||
if os.path.exists("%s/actionsmap/" % CACHE_DIR):
|
||||
# clean old cached files
|
||||
for i in os.listdir("%s/actionsmap/" % CACHE_DIR):
|
||||
if i.endswith(".pkl"):
|
||||
os.remove("%s/actionsmap/%s" % (CACHE_DIR, i))
|
||||
# at installation, cachedir might not exists
|
||||
for old_cache in glob.glob("%s/actionsmap/%s-*.pkl" % (CACHE_DIR, namespace)):
|
||||
os.remove(old_cache)
|
||||
|
||||
# Cache actions map into pickle file
|
||||
am_file_stat = os.stat(am_file)
|
||||
# Cache actions map into pickle file
|
||||
am_file_stat = os.stat(am_file)
|
||||
|
||||
pkl = "%s-%d-%d.pkl" % (n, am_file_stat.st_size, am_file_stat.st_mtime)
|
||||
pkl = "%s-%d-%d.pkl" % (namespace, am_file_stat.st_size, am_file_stat.st_mtime)
|
||||
|
||||
with open_cachefile(pkl, "wb", subdir="actionsmap") as f:
|
||||
pickle.dump(actionsmaps[n], f)
|
||||
with open_cachefile(pkl, "wb", subdir="actionsmap") as f:
|
||||
pickle.dump(actionsmap, f)
|
||||
|
||||
return actionsmaps
|
||||
return actionsmap
|
||||
|
||||
# Private methods
|
||||
|
||||
def _construct_parser(self, actionsmaps, **kwargs):
|
||||
def _construct_parser(self, actionsmaps, top_parser):
|
||||
"""
|
||||
Construct the parser with the actions map
|
||||
|
||||
Keyword arguments:
|
||||
- actionsmaps -- A dict of multi-level dictionnary of
|
||||
categories/actions/arguments list for each namespaces
|
||||
- **kwargs -- Additionnal arguments to pass at the parser
|
||||
class instantiation
|
||||
- top_parser -- A BaseActionsMapParser-derived instance to use for
|
||||
parsing the actions map
|
||||
|
||||
Returns:
|
||||
An interface relevant's parser object
|
||||
|
||||
"""
|
||||
# Get extra parameters
|
||||
if self.use_cache:
|
||||
validate_extra = False
|
||||
else:
|
||||
validate_extra = True
|
||||
|
||||
# Instantiate parser
|
||||
#
|
||||
# this either returns:
|
||||
# * moulinette.interfaces.cli.ActionsMapParser
|
||||
# * moulinette.interfaces.api.ActionsMapParser
|
||||
top_parser = self.parser_class(**kwargs)
|
||||
logger.debug("building parser...")
|
||||
start = time()
|
||||
|
||||
# If loading from cache, extra were already checked when cache was
|
||||
# loaded ? Not sure about this ... old code is a bit mysterious...
|
||||
validate_extra = not self.from_cache
|
||||
|
||||
# namespace, actionsmap is a tuple where:
|
||||
#
|
||||
|
@ -781,4 +799,5 @@ class ActionsMap(object):
|
|||
tid, action_options["configuration"]
|
||||
)
|
||||
|
||||
logger.debug("building parser took %.3fs", time() - start)
|
||||
return top_parser
|
||||
|
|
|
@ -5,7 +5,7 @@ import logging
|
|||
import hashlib
|
||||
import hmac
|
||||
|
||||
from moulinette.cache import open_cachefile, get_cachedir
|
||||
from moulinette.cache import open_cachefile, get_cachedir, cachefile_exists
|
||||
from moulinette.core import MoulinetteError
|
||||
|
||||
logger = logging.getLogger("moulinette.authenticator")
|
||||
|
@ -159,6 +159,10 @@ class BaseAuthenticator(object):
|
|||
"%s.asc" % session_id, mode, subdir="session/%s" % self.name
|
||||
)
|
||||
|
||||
def _session_exists(self, session_id):
|
||||
"""Check a session exists"""
|
||||
return cachefile_exists("%s.asc" % session_id, subdir="session/%s" % self.name)
|
||||
|
||||
def _store_session(self, session_id, session_token):
|
||||
"""Store a session to be able to use it later to reauthenticate"""
|
||||
|
||||
|
@ -170,6 +174,8 @@ class BaseAuthenticator(object):
|
|||
|
||||
def _authenticate_session(self, session_id, session_token):
|
||||
"""Checks session and token against the stored session token"""
|
||||
if not self._session_exists(session_id):
|
||||
raise MoulinetteError("session_expired")
|
||||
try:
|
||||
# FIXME : shouldn't we also add a check that this session file
|
||||
# is not too old ? e.g. not older than 24 hours ? idk...
|
||||
|
|
|
@ -2,15 +2,15 @@
|
|||
|
||||
# TODO: Use Python3 to remove this fix!
|
||||
from __future__ import absolute_import
|
||||
import os
|
||||
import logging
|
||||
import random
|
||||
import string
|
||||
import crypt
|
||||
import ldap
|
||||
import ldap.sasl
|
||||
import time
|
||||
import ldap.modlist as modlist
|
||||
|
||||
from moulinette.core import MoulinetteError
|
||||
from moulinette import m18n
|
||||
from moulinette.core import MoulinetteError, MoulinetteLdapIsDownError
|
||||
from moulinette.authenticators import BaseAuthenticator
|
||||
|
||||
logger = logging.getLogger("moulinette.authenticator.ldap")
|
||||
|
@ -69,7 +69,7 @@ class Authenticator(BaseAuthenticator):
|
|||
# Implement virtual methods
|
||||
|
||||
def authenticate(self, password=None):
|
||||
try:
|
||||
def _reconnect():
|
||||
con = ldap.ldapobject.ReconnectLDAPObject(
|
||||
self._get_uri(), retry_max=10, retry_delay=0.5
|
||||
)
|
||||
|
@ -80,11 +80,23 @@ class Authenticator(BaseAuthenticator):
|
|||
con.simple_bind_s(self.userdn, password)
|
||||
else:
|
||||
con.simple_bind_s()
|
||||
|
||||
return con
|
||||
|
||||
try:
|
||||
con = _reconnect()
|
||||
except ldap.INVALID_CREDENTIALS:
|
||||
raise MoulinetteError("invalid_password")
|
||||
except ldap.SERVER_DOWN:
|
||||
logger.exception("unable to reach the server to authenticate")
|
||||
raise MoulinetteError("ldap_server_down")
|
||||
# ldap is down, attempt to restart it before really failing
|
||||
logger.warning(m18n.g("ldap_server_is_down_restart_it"))
|
||||
os.system("systemctl restart slapd")
|
||||
time.sleep(10) # waits 10 secondes so we are sure that slapd has restarted
|
||||
|
||||
try:
|
||||
con = _reconnect()
|
||||
except ldap.SERVER_DOWN:
|
||||
raise MoulinetteLdapIsDownError("ldap_server_down")
|
||||
|
||||
# Check that we are indeed logged in with the right identity
|
||||
try:
|
||||
|
@ -99,30 +111,6 @@ class Authenticator(BaseAuthenticator):
|
|||
raise MoulinetteError("Not logged in with the expected userdn ?!")
|
||||
else:
|
||||
self.con = con
|
||||
self._ensure_password_uses_strong_hash(password)
|
||||
|
||||
def _ensure_password_uses_strong_hash(self, password):
|
||||
# XXX this has been copy pasted from YunoHost, should we put that into moulinette?
|
||||
def _hash_user_password(password):
|
||||
char_set = (
|
||||
string.ascii_uppercase + string.ascii_lowercase + string.digits + "./"
|
||||
)
|
||||
salt = "".join([random.SystemRandom().choice(char_set) for x in range(16)])
|
||||
salt = "$6$" + salt + "$"
|
||||
return "{CRYPT}" + crypt.crypt(str(password), salt)
|
||||
|
||||
hashed_password = self.search(self.admindn, attrs=["userPassword"])[0]
|
||||
|
||||
# post-install situation, password is not already set
|
||||
if "userPassword" not in hashed_password or not hashed_password["userPassword"]:
|
||||
return
|
||||
|
||||
# we aren't using sha-512 but something else that is weaker, proceed to upgrade
|
||||
if not hashed_password["userPassword"][0].startswith("{CRYPT}$6$"):
|
||||
self.update(
|
||||
"cn=%s" % self.adminuser,
|
||||
{"userPassword": [_hash_user_password(password)]},
|
||||
)
|
||||
|
||||
# Additional LDAP methods
|
||||
# TODO: Review these methods
|
||||
|
@ -148,15 +136,11 @@ class Authenticator(BaseAuthenticator):
|
|||
try:
|
||||
result = self.con.search_s(base, ldap.SCOPE_SUBTREE, filter, attrs)
|
||||
except Exception as e:
|
||||
logger.exception(
|
||||
raise MoulinetteError(
|
||||
"error during LDAP search operation with: base='%s', "
|
||||
"filter='%s', attrs=%s and exception %s",
|
||||
base,
|
||||
filter,
|
||||
attrs,
|
||||
e,
|
||||
"filter='%s', attrs=%s and exception %s" % (base, filter, attrs, e),
|
||||
raw_msg=True,
|
||||
)
|
||||
raise MoulinetteError("ldap_operation_error", action="search")
|
||||
|
||||
result_list = []
|
||||
if not attrs or "dn" not in attrs:
|
||||
|
@ -185,14 +169,11 @@ class Authenticator(BaseAuthenticator):
|
|||
try:
|
||||
self.con.add_s(dn, ldif)
|
||||
except Exception as e:
|
||||
logger.exception(
|
||||
raise MoulinetteError(
|
||||
"error during LDAP add operation with: rdn='%s', "
|
||||
"attr_dict=%s and exception %s",
|
||||
rdn,
|
||||
attr_dict,
|
||||
e,
|
||||
"attr_dict=%s and exception %s" % (rdn, attr_dict, e),
|
||||
raw_msg=True,
|
||||
)
|
||||
raise MoulinetteError("ldap_operation_error", action="add")
|
||||
else:
|
||||
return True
|
||||
|
||||
|
@ -211,12 +192,11 @@ class Authenticator(BaseAuthenticator):
|
|||
try:
|
||||
self.con.delete_s(dn)
|
||||
except Exception as e:
|
||||
logger.exception(
|
||||
"error during LDAP delete operation with: rdn='%s' and exception %s",
|
||||
rdn,
|
||||
e,
|
||||
raise MoulinetteError(
|
||||
"error during LDAP delete operation with: rdn='%s' and exception %s"
|
||||
% (rdn, e),
|
||||
raw_msg=True,
|
||||
)
|
||||
raise MoulinetteError("ldap_operation_error", action="remove")
|
||||
else:
|
||||
return True
|
||||
|
||||
|
@ -249,15 +229,12 @@ class Authenticator(BaseAuthenticator):
|
|||
|
||||
self.con.modify_ext_s(dn, ldif)
|
||||
except Exception as e:
|
||||
logger.exception(
|
||||
raise MoulinetteError(
|
||||
"error during LDAP update operation with: rdn='%s', "
|
||||
"attr_dict=%s, new_rdn=%s and exception: %s",
|
||||
rdn,
|
||||
attr_dict,
|
||||
new_rdn,
|
||||
e,
|
||||
"attr_dict=%s, new_rdn=%s and exception: %s"
|
||||
% (rdn, attr_dict, new_rdn, e),
|
||||
raw_msg=True,
|
||||
)
|
||||
raise MoulinetteError("ldap_operation_error", action="update")
|
||||
else:
|
||||
return True
|
||||
|
||||
|
|
|
@ -42,3 +42,10 @@ def open_cachefile(filename, mode="r", subdir=""):
|
|||
cache_dir = get_cachedir(subdir, make_dir=True if mode[0] == "w" else False)
|
||||
file_path = os.path.join(cache_dir, filename)
|
||||
return open(file_path, mode)
|
||||
|
||||
|
||||
def cachefile_exists(filename, subdir=""):
|
||||
|
||||
cache_dir = get_cachedir(subdir, make_dir=False)
|
||||
file_path = os.path.join(cache_dir, filename)
|
||||
return os.path.exists(file_path)
|
||||
|
|
|
@ -5,8 +5,6 @@ import time
|
|||
import json
|
||||
import logging
|
||||
|
||||
from importlib import import_module
|
||||
|
||||
import moulinette
|
||||
from moulinette.globals import init_moulinette_env
|
||||
|
||||
|
@ -372,53 +370,6 @@ class MoulinetteSignals(object):
|
|||
raise NotImplementedError("this signal is not handled")
|
||||
|
||||
|
||||
# Interfaces & Authenticators management -------------------------------
|
||||
|
||||
|
||||
def init_interface(name, kwargs={}, actionsmap={}):
|
||||
"""Return a new interface instance
|
||||
|
||||
Retrieve the given interface module and return a new instance of its
|
||||
Interface class. It is initialized with arguments 'kwargs' and
|
||||
connected to 'actionsmap' if it's an ActionsMap object, otherwise
|
||||
a new ActionsMap instance will be initialized with arguments
|
||||
'actionsmap'.
|
||||
|
||||
Keyword arguments:
|
||||
- name -- The interface name
|
||||
- kwargs -- A dict of arguments to pass to Interface
|
||||
- actionsmap -- Either an ActionsMap instance or a dict of
|
||||
arguments to pass to ActionsMap
|
||||
|
||||
"""
|
||||
from moulinette.actionsmap import ActionsMap
|
||||
|
||||
try:
|
||||
mod = import_module("moulinette.interfaces.%s" % name)
|
||||
except ImportError as e:
|
||||
logger.exception("unable to load interface '%s' : %s", name, e)
|
||||
raise MoulinetteError("error_see_log")
|
||||
else:
|
||||
try:
|
||||
# Retrieve interface classes
|
||||
parser = mod.ActionsMapParser
|
||||
interface = mod.Interface
|
||||
except AttributeError:
|
||||
logger.exception("unable to retrieve classes of interface '%s'", name)
|
||||
raise MoulinetteError("error_see_log")
|
||||
|
||||
# Instantiate or retrieve ActionsMap
|
||||
if isinstance(actionsmap, dict):
|
||||
amap = ActionsMap(actionsmap.pop("parser", parser), **actionsmap)
|
||||
elif isinstance(actionsmap, ActionsMap):
|
||||
amap = actionsmap
|
||||
else:
|
||||
logger.error("invalid actionsmap value %r", actionsmap)
|
||||
raise MoulinetteError("error_see_log")
|
||||
|
||||
return interface(amap, **kwargs)
|
||||
|
||||
|
||||
# Moulinette core classes ----------------------------------------------
|
||||
|
||||
|
||||
|
@ -435,6 +386,10 @@ class MoulinetteError(Exception):
|
|||
self.strerror = msg
|
||||
|
||||
|
||||
class MoulinetteLdapIsDownError(MoulinetteError):
|
||||
"""Used when ldap is down"""
|
||||
|
||||
|
||||
class MoulinetteLock(object):
|
||||
|
||||
"""Locker for a moulinette instance
|
||||
|
|
|
@ -11,4 +11,7 @@ def init_moulinette_env():
|
|||
"MOULINETTE_LOCALES_DIR", "/usr/share/moulinette/locale"
|
||||
),
|
||||
"CACHE_DIR": environ.get("MOULINETTE_CACHE_DIR", "/var/cache/moulinette"),
|
||||
"NAMESPACES": environ.get(
|
||||
"MOULINETTE_NAMESPACES", "*"
|
||||
).split(), # By default we'll load every namespace we find
|
||||
}
|
||||
|
|
|
@ -342,11 +342,6 @@ class _CallbackAction(argparse.Action):
|
|||
self.callback_method = callback.get("method")
|
||||
self.callback_kwargs = callback.get("kwargs", {})
|
||||
self.callback_return = callback.get("return", False)
|
||||
logger.debug(
|
||||
"registering new callback action '{0}' to {1}".format(
|
||||
self.callback_method, option_strings
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
def callback(self):
|
||||
|
@ -361,6 +356,9 @@ class _CallbackAction(argparse.Action):
|
|||
mod = __import__(mod_name, globals=globals(), level=0, fromlist=[func_name])
|
||||
func = getattr(mod, func_name)
|
||||
except (AttributeError, ImportError):
|
||||
import traceback
|
||||
|
||||
traceback.print_exc()
|
||||
raise ValueError("unable to import method {0}".format(self.callback_method))
|
||||
self._callback = func
|
||||
|
||||
|
|
|
@ -10,10 +10,11 @@ from gevent import sleep
|
|||
from gevent.queue import Queue
|
||||
from geventwebsocket import WebSocketError
|
||||
|
||||
from bottle import run, request, response, Bottle, HTTPResponse
|
||||
from bottle import request, response, Bottle, HTTPResponse
|
||||
from bottle import abort
|
||||
|
||||
from moulinette import msignals, m18n, env
|
||||
from moulinette.actionsmap import ActionsMap
|
||||
from moulinette.core import MoulinetteError
|
||||
from moulinette.interfaces import (
|
||||
BaseActionsMapParser,
|
||||
|
@ -219,22 +220,18 @@ class _ActionsMapPlugin(object):
|
|||
|
||||
Keyword arguments:
|
||||
- actionsmap -- An ActionsMap instance
|
||||
- use_websocket -- If true, install a WebSocket on /messages in order
|
||||
to serve messages coming from the 'display' signal
|
||||
|
||||
"""
|
||||
|
||||
name = "actionsmap"
|
||||
api = 2
|
||||
|
||||
def __init__(self, actionsmap, use_websocket, log_queues={}):
|
||||
def __init__(self, actionsmap, log_queues={}):
|
||||
# Connect signals to handlers
|
||||
msignals.set_handler("authenticate", self._do_authenticate)
|
||||
if use_websocket:
|
||||
msignals.set_handler("display", self._do_display)
|
||||
msignals.set_handler("display", self._do_display)
|
||||
|
||||
self.actionsmap = actionsmap
|
||||
self.use_websocket = use_websocket
|
||||
self.log_queues = log_queues
|
||||
# TODO: Save and load secrets?
|
||||
self.secrets = {}
|
||||
|
@ -290,13 +287,9 @@ class _ActionsMapPlugin(object):
|
|||
)
|
||||
|
||||
# Append messages route
|
||||
if self.use_websocket:
|
||||
app.route(
|
||||
"/messages",
|
||||
name="messages",
|
||||
callback=self.messages,
|
||||
skip=["actionsmap"],
|
||||
)
|
||||
app.route(
|
||||
"/messages", name="messages", callback=self.messages, skip=["actionsmap"],
|
||||
)
|
||||
|
||||
# Append routes from the actions map
|
||||
for (m, p) in self.actionsmap.parser.routes:
|
||||
|
@ -327,11 +320,10 @@ class _ActionsMapPlugin(object):
|
|||
# Append other request params
|
||||
for k, v in request.params.dict.items():
|
||||
v = _format(v)
|
||||
try:
|
||||
curr_v = params[k]
|
||||
except KeyError:
|
||||
if k not in params.keys():
|
||||
params[k] = v
|
||||
else:
|
||||
curr_v = params[k]
|
||||
# Append param value to the list
|
||||
if not isinstance(curr_v, list):
|
||||
curr_v = [curr_v]
|
||||
|
@ -362,13 +354,24 @@ class _ActionsMapPlugin(object):
|
|||
|
||||
"""
|
||||
# Retrieve session values
|
||||
s_id = request.get_cookie("session.id") or random_ascii()
|
||||
try:
|
||||
s_id = request.get_cookie("session.id") or random_ascii()
|
||||
except:
|
||||
# Super rare case where there are super weird cookie / cache issue
|
||||
# Previous line throws a CookieError that creates a 500 error ...
|
||||
# So let's catch it and just use a fresh ID then...
|
||||
s_id = random_ascii()
|
||||
|
||||
try:
|
||||
s_secret = self.secrets[s_id]
|
||||
except KeyError:
|
||||
s_tokens = {}
|
||||
else:
|
||||
s_tokens = request.get_cookie("session.tokens", secret=s_secret) or {}
|
||||
try:
|
||||
s_tokens = request.get_cookie("session.tokens", secret=s_secret) or {}
|
||||
except:
|
||||
# Same as for session.id a few lines before
|
||||
s_tokens = {}
|
||||
s_new_token = random_ascii()
|
||||
|
||||
try:
|
||||
|
@ -574,6 +577,9 @@ def format_for_response(content):
|
|||
return ""
|
||||
response.status = 200
|
||||
|
||||
if isinstance(content, HTTPResponse):
|
||||
return content
|
||||
|
||||
# Return JSON-style response
|
||||
response.content_type = "application/json"
|
||||
return json_encode(content, cls=JSONExtendedEncoder)
|
||||
|
@ -735,17 +741,16 @@ class Interface(BaseInterface):
|
|||
actions map.
|
||||
|
||||
Keyword arguments:
|
||||
- actionsmap -- The ActionsMap instance to connect to
|
||||
- routes -- A dict of additional routes to add in the form of
|
||||
{(method, path): callback}
|
||||
- use_websocket -- Serve via WSGI to handle asynchronous responses
|
||||
- log_queues -- A LogQueues object or None to retrieve it from
|
||||
registered logging handlers
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, actionsmap, routes={}, use_websocket=True, log_queues=None):
|
||||
self.use_websocket = use_websocket
|
||||
def __init__(self, routes={}, log_queues=None):
|
||||
|
||||
actionsmap = ActionsMap(ActionsMapParser())
|
||||
|
||||
# Attempt to retrieve log queues from an APIQueueHandler
|
||||
if log_queues is None:
|
||||
|
@ -766,18 +771,21 @@ class Interface(BaseInterface):
|
|||
|
||||
# Attempt to retrieve and set locale
|
||||
def api18n(callback):
|
||||
try:
|
||||
locale = request.params.pop("locale")
|
||||
except KeyError:
|
||||
locale = m18n.default_locale
|
||||
m18n.set_locale(locale)
|
||||
return callback
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
locale = request.params.pop("locale")
|
||||
except KeyError:
|
||||
locale = m18n.default_locale
|
||||
m18n.set_locale(locale)
|
||||
return callback(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
# Install plugins
|
||||
app.install(filter_csrf)
|
||||
app.install(apiheader)
|
||||
app.install(api18n)
|
||||
app.install(_ActionsMapPlugin(actionsmap, use_websocket, log_queues))
|
||||
app.install(_ActionsMapPlugin(actionsmap, log_queues))
|
||||
|
||||
# Append default routes
|
||||
# app.route(['/api', '/api/<category:re:[a-z]+>'], method='GET',
|
||||
|
@ -802,23 +810,15 @@ class Interface(BaseInterface):
|
|||
|
||||
"""
|
||||
logger.debug(
|
||||
"starting the server instance in %s:%d with websocket=%s",
|
||||
host,
|
||||
port,
|
||||
self.use_websocket,
|
||||
"starting the server instance in %s:%d", host, port,
|
||||
)
|
||||
|
||||
try:
|
||||
if self.use_websocket:
|
||||
from gevent.pywsgi import WSGIServer
|
||||
from geventwebsocket.handler import WebSocketHandler
|
||||
from gevent.pywsgi import WSGIServer
|
||||
from geventwebsocket.handler import WebSocketHandler
|
||||
|
||||
server = WSGIServer(
|
||||
(host, port), self._app, handler_class=WebSocketHandler
|
||||
)
|
||||
server.serve_forever()
|
||||
else:
|
||||
run(self._app, host=host, port=port)
|
||||
server = WSGIServer((host, port), self._app, handler_class=WebSocketHandler)
|
||||
server.serve_forever()
|
||||
except IOError as e:
|
||||
logger.exception("unable to start the server instance on %s:%d", host, port)
|
||||
if e.args[0] == errno.EADDRINUSE:
|
||||
|
|
|
@ -7,12 +7,12 @@ import locale
|
|||
import logging
|
||||
from argparse import SUPPRESS
|
||||
from collections import OrderedDict
|
||||
import pytz
|
||||
from datetime import date, datetime
|
||||
|
||||
import argcomplete
|
||||
|
||||
from moulinette import msignals, m18n
|
||||
from moulinette.actionsmap import ActionsMap
|
||||
from moulinette.core import MoulinetteError
|
||||
from moulinette.interfaces import (
|
||||
BaseActionsMapParser,
|
||||
|
@ -103,6 +103,8 @@ def pretty_date(_date):
|
|||
Argument:
|
||||
- date -- The date or datetime to display
|
||||
"""
|
||||
import pytz # Lazy loading, this takes like 3+ sec on a RPi2 ?!
|
||||
|
||||
# Deduce system timezone
|
||||
nowutc = datetime.now(tz=pytz.utc)
|
||||
nowtz = datetime.now()
|
||||
|
@ -168,8 +170,13 @@ def pretty_print_dict(d, depth=0):
|
|||
|
||||
|
||||
def get_locale():
|
||||
"""Return current user locale"""
|
||||
lang = locale.getdefaultlocale()[0]
|
||||
"""Return current user eocale"""
|
||||
try:
|
||||
lang = locale.getdefaultlocale()[0]
|
||||
except Exception:
|
||||
# In some edge case the locale lib fails ...
|
||||
# c.f. https://forum.yunohost.org/t/error-when-trying-to-enter-user-information-in-admin-panel/11390/11
|
||||
lang = os.getenv("LANG")
|
||||
if not lang:
|
||||
return ""
|
||||
return lang[:2]
|
||||
|
@ -418,7 +425,8 @@ class Interface(BaseInterface):
|
|||
|
||||
"""
|
||||
|
||||
def __init__(self, actionsmap):
|
||||
def __init__(self, top_parser=None, load_only_category=None):
|
||||
|
||||
# Set user locale
|
||||
m18n.set_locale(get_locale())
|
||||
|
||||
|
@ -428,9 +436,12 @@ class Interface(BaseInterface):
|
|||
msignals.set_handler("authenticate", self._do_authenticate)
|
||||
msignals.set_handler("prompt", self._do_prompt)
|
||||
|
||||
self.actionsmap = actionsmap
|
||||
self.actionsmap = ActionsMap(
|
||||
ActionsMapParser(top_parser=top_parser),
|
||||
load_only_category=load_only_category,
|
||||
)
|
||||
|
||||
def run(self, args, output_as=None, password=None, timeout=None):
|
||||
def run(self, args, output_as=None, timeout=None):
|
||||
"""Run the moulinette
|
||||
|
||||
Process the action corresponding to the given arguments 'args'
|
||||
|
@ -442,7 +453,6 @@ class Interface(BaseInterface):
|
|||
- json: return a JSON encoded string
|
||||
- plain: return a script-readable output
|
||||
- none: do not output the result
|
||||
- password -- The password to use in case of authentication
|
||||
- timeout -- Number of seconds before this command will timeout because it can't acquire the lock (meaning that another command is currently running), by default there is no timeout and the command will wait until it can get the lock
|
||||
|
||||
"""
|
||||
|
@ -453,11 +463,7 @@ class Interface(BaseInterface):
|
|||
argcomplete.autocomplete(self.actionsmap.parser._parser)
|
||||
|
||||
# Set handler for authentication
|
||||
if password:
|
||||
msignals.set_handler("authenticate", lambda a: a(password=password))
|
||||
else:
|
||||
if os.isatty(1):
|
||||
msignals.set_handler("authenticate", self._do_authenticate)
|
||||
msignals.set_handler("authenticate", self._do_authenticate)
|
||||
|
||||
try:
|
||||
ret = self.actionsmap.process(args, timeout=timeout)
|
||||
|
@ -489,7 +495,11 @@ class Interface(BaseInterface):
|
|||
Handle the core.MoulinetteSignals.authenticate signal.
|
||||
|
||||
"""
|
||||
# TODO: Allow token authentication?
|
||||
# Hmpf we have no-use case in yunohost anymore where we need to auth
|
||||
# because everything is run as root ...
|
||||
# I guess we could imagine some yunohost-independant use-case where
|
||||
# moulinette is used to create a CLI for non-root user that needs to
|
||||
# auth somehow but hmpf -.-
|
||||
help = authenticator.extra.get("help")
|
||||
msg = m18n.n(help) if help else m18n.g("password")
|
||||
return authenticator(password=self._do_prompt(msg, True, False, color="yellow"))
|
||||
|
|
|
@ -65,6 +65,7 @@ def configure_logging(logging_config=None):
|
|||
# load configuration from dict
|
||||
dictConfig(DEFAULT_LOGGING)
|
||||
if logging_config:
|
||||
logging.main_logger = logging_config.get("main_logger")
|
||||
dictConfig(logging_config)
|
||||
|
||||
|
||||
|
|
|
@ -28,7 +28,7 @@ def check_output(args, stderr=subprocess.STDOUT, shell=True, **kwargs):
|
|||
and use shell by default before calling subprocess.check_output.
|
||||
|
||||
"""
|
||||
return subprocess.check_output(args, stderr=stderr, shell=shell, **kwargs)
|
||||
return subprocess.check_output(args, stderr=stderr, shell=shell, **kwargs).strip()
|
||||
|
||||
|
||||
# Call with stream access ----------------------------------------------
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import logging
|
||||
from json.encoder import JSONEncoder
|
||||
import datetime
|
||||
import pytz
|
||||
|
||||
logger = logging.getLogger("moulinette.utils.serialize")
|
||||
|
||||
|
@ -23,6 +22,9 @@ class JSONExtendedEncoder(JSONEncoder):
|
|||
"""
|
||||
|
||||
def default(self, o):
|
||||
|
||||
import pytz # Lazy loading, this takes like 3+ sec on a RPi2 ?!
|
||||
|
||||
"""Return a serializable object"""
|
||||
# Convert compatible containers into list
|
||||
if isinstance(o, set) or (hasattr(o, "__iter__") and hasattr(o, "next")):
|
||||
|
|
49
setup.py
49
setup.py
|
@ -17,6 +17,31 @@ if "install" in sys.argv:
|
|||
if f.endswith('.json'):
|
||||
locale_files.append('locales/%s' % f)
|
||||
|
||||
install_deps = [
|
||||
'argcomplete',
|
||||
'psutil',
|
||||
'pytz',
|
||||
'pyyaml',
|
||||
'toml',
|
||||
'python-ldap',
|
||||
'gevent-websocket',
|
||||
'bottle',
|
||||
]
|
||||
|
||||
test_deps = [
|
||||
'pytest',
|
||||
'pytest-cov',
|
||||
'pytest-env',
|
||||
'pytest-mock',
|
||||
'requests',
|
||||
'requests-mock',
|
||||
'webtest'
|
||||
]
|
||||
extras = {
|
||||
'install': install_deps,
|
||||
'tests': test_deps,
|
||||
}
|
||||
|
||||
|
||||
setup(name='Moulinette',
|
||||
version='2.0.0',
|
||||
|
@ -27,24 +52,8 @@ setup(name='Moulinette',
|
|||
license='AGPL',
|
||||
packages=find_packages(exclude=['test']),
|
||||
data_files=[(LOCALES_DIR, locale_files)],
|
||||
python_requires='>=3.5',
|
||||
install_requires=[
|
||||
'argcomplete',
|
||||
'psutil',
|
||||
'pytz',
|
||||
'pyyaml',
|
||||
'toml',
|
||||
'python-ldap',
|
||||
'gevent-websocket',
|
||||
'bottle',
|
||||
],
|
||||
tests_require=[
|
||||
'pytest',
|
||||
'pytest-cov',
|
||||
'pytest-env',
|
||||
'pytest-mock',
|
||||
'requests',
|
||||
'requests-mock',
|
||||
'webtest'
|
||||
],
|
||||
python_requires='>=2.7.*, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4',
|
||||
install_requires=install_deps,
|
||||
tests_require=test_deps,
|
||||
extras_require=extras,
|
||||
)
|
||||
|
|
|
@ -7,7 +7,7 @@ import os
|
|||
import shutil
|
||||
import pytest
|
||||
|
||||
from src.ldap_server import LDAPServer
|
||||
from .src.ldap_server import LDAPServer
|
||||
|
||||
|
||||
def patch_init(moulinette):
|
||||
|
@ -125,13 +125,9 @@ def moulinette_webapi(moulinette):
|
|||
|
||||
CookiePolicy.return_ok_secure = return_true
|
||||
|
||||
moulinette_webapi = moulinette.core.init_interface(
|
||||
"api",
|
||||
kwargs={"routes": {}, "use_websocket": False},
|
||||
actionsmap={"namespaces": ["moulitest"], "use_cache": True},
|
||||
)
|
||||
from moulinette.interfaces.api import Interface as Api
|
||||
|
||||
return TestApp(moulinette_webapi._app)
|
||||
return TestApp(Api(routes={})._app)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
@ -148,17 +144,12 @@ def moulinette_cli(moulinette, mocker):
|
|||
help="Log and print debug messages",
|
||||
)
|
||||
mocker.patch("os.isatty", return_value=True)
|
||||
moulinette_cli = moulinette.core.init_interface(
|
||||
"cli",
|
||||
actionsmap={
|
||||
"namespaces": ["moulitest"],
|
||||
"use_cache": False,
|
||||
"parser_kwargs": {"top_parser": parser},
|
||||
},
|
||||
)
|
||||
from moulinette.interfaces.cli import Interface as Cli
|
||||
|
||||
cli = Cli(top_parser=parser)
|
||||
mocker.stopall()
|
||||
|
||||
return moulinette_cli
|
||||
return cli
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
|
|
@ -98,7 +98,23 @@ class LDAPServer:
|
|||
"posixAccount",
|
||||
"simpleSecurityObject",
|
||||
],
|
||||
"userPassword": ["yunohost"],
|
||||
"userPassword": [self._hash_user_password("yunohost")],
|
||||
}
|
||||
|
||||
ldap_interface.update("cn=admin", admin_dict)
|
||||
|
||||
def _hash_user_password(self, password):
|
||||
"""
|
||||
Copy pasta of what's in yunohost/user.py
|
||||
"""
|
||||
import string
|
||||
import random
|
||||
import crypt
|
||||
|
||||
char_set = (
|
||||
string.ascii_uppercase + string.ascii_lowercase + string.digits + "./"
|
||||
)
|
||||
salt = "".join([random.SystemRandom().choice(char_set) for x in range(16)])
|
||||
|
||||
salt = "$6$" + salt + "$"
|
||||
return "{CRYPT}" + crypt.crypt(str(password), salt)
|
||||
|
|
|
@ -158,10 +158,10 @@ def test_required_paremeter_missing_value(iface, caplog):
|
|||
|
||||
def test_actions_map_unknown_authenticator(monkeypatch, tmp_path):
|
||||
monkeypatch.setenv("MOULINETTE_DATA_DIR", str(tmp_path))
|
||||
actionsmap_dir = actionsmap_dir = tmp_path / "actionsmap"
|
||||
actionsmap_dir = tmp_path / "actionsmap"
|
||||
actionsmap_dir.mkdir()
|
||||
|
||||
amap = ActionsMap(BaseActionsMapParser)
|
||||
amap = ActionsMap(BaseActionsMapParser())
|
||||
with pytest.raises(ValueError) as exception:
|
||||
amap.get_authenticator_for_profile("unknown")
|
||||
assert "Unknown authenticator" in str(exception)
|
||||
|
@ -225,7 +225,7 @@ def test_extra_argument_parser_parse_args(iface, mocker):
|
|||
def test_actions_map_api():
|
||||
from moulinette.interfaces.api import ActionsMapParser
|
||||
|
||||
amap = ActionsMap(ActionsMapParser, use_cache=False)
|
||||
amap = ActionsMap(ActionsMapParser())
|
||||
|
||||
assert amap.parser.global_conf["authenticate"] == "all"
|
||||
assert "default" in amap.parser.global_conf["authenticator"]
|
||||
|
@ -233,9 +233,9 @@ def test_actions_map_api():
|
|||
assert ("GET", "/test-auth/default") in amap.parser.routes
|
||||
assert ("POST", "/test-auth/subcat/post") in amap.parser.routes
|
||||
|
||||
amap.generate_cache()
|
||||
amap.generate_cache("moulitest")
|
||||
|
||||
amap = ActionsMap(ActionsMapParser, use_cache=True)
|
||||
amap = ActionsMap(ActionsMapParser())
|
||||
|
||||
assert amap.parser.global_conf["authenticate"] == "all"
|
||||
assert "default" in amap.parser.global_conf["authenticator"]
|
||||
|
@ -247,17 +247,24 @@ def test_actions_map_api():
|
|||
def test_actions_map_import_error(mocker):
|
||||
from moulinette.interfaces.api import ActionsMapParser
|
||||
|
||||
amap = ActionsMap(ActionsMapParser)
|
||||
amap = ActionsMap(ActionsMapParser())
|
||||
|
||||
from moulinette.core import MoulinetteLock
|
||||
|
||||
mocker.patch.object(MoulinetteLock, "_is_son_of", return_value=False)
|
||||
|
||||
mocker.patch("__builtin__.__import__", side_effect=ImportError)
|
||||
orig_import = __import__
|
||||
|
||||
def import_mock(name, globals={}, locals={}, fromlist=[], level=-1):
|
||||
if name == "moulitest.testauth":
|
||||
mocker.stopall()
|
||||
raise ImportError
|
||||
return orig_import(name, globals, locals, fromlist, level)
|
||||
|
||||
mocker.patch("__builtin__.__import__", side_effect=import_mock)
|
||||
with pytest.raises(MoulinetteError) as exception:
|
||||
amap.process({}, timeout=30, route=("GET", "/test-auth/none"))
|
||||
|
||||
mocker.stopall()
|
||||
translation = m18n.g("error_see_log")
|
||||
expected_msg = translation.format()
|
||||
assert expected_msg in str(exception)
|
||||
|
@ -274,9 +281,7 @@ def test_actions_map_cli():
|
|||
default=False,
|
||||
help="Log and print debug messages",
|
||||
)
|
||||
amap = ActionsMap(
|
||||
ActionsMapParser, use_cache=False, parser_kwargs={"top_parser": parser}
|
||||
)
|
||||
amap = ActionsMap(ActionsMapParser(top_parser=parser))
|
||||
|
||||
assert amap.parser.global_conf["authenticate"] == "all"
|
||||
assert "default" in amap.parser.global_conf["authenticator"]
|
||||
|
@ -293,11 +298,9 @@ def test_actions_map_cli():
|
|||
.choices
|
||||
)
|
||||
|
||||
amap.generate_cache()
|
||||
amap.generate_cache("moulitest")
|
||||
|
||||
amap = ActionsMap(
|
||||
ActionsMapParser, use_cache=True, parser_kwargs={"top_parser": parser}
|
||||
)
|
||||
amap = ActionsMap(ActionsMapParser(top_parser=parser))
|
||||
|
||||
assert amap.parser.global_conf["authenticate"] == "all"
|
||||
assert "default" in amap.parser.global_conf["authenticator"]
|
||||
|
|
|
@ -216,18 +216,15 @@ class TestAuthCLI:
|
|||
|
||||
assert "some_data_from_default" in message.out
|
||||
|
||||
moulinette_cli.run(
|
||||
["testauth", "default"], output_as="plain", password="default"
|
||||
)
|
||||
moulinette_cli.run(["testauth", "default"], output_as="plain")
|
||||
message = capsys.readouterr()
|
||||
|
||||
assert "some_data_from_default" in message.out
|
||||
|
||||
def test_login_bad_password(self, moulinette_cli, capsys, mocker):
|
||||
mocker.patch("getpass.getpass", return_value="Bad Password")
|
||||
with pytest.raises(MoulinetteError):
|
||||
moulinette_cli.run(
|
||||
["testauth", "default"], output_as="plain", password="Bad Password"
|
||||
)
|
||||
moulinette_cli.run(["testauth", "default"], output_as="plain")
|
||||
|
||||
mocker.patch("getpass.getpass", return_value="Bad Password")
|
||||
with pytest.raises(MoulinetteError):
|
||||
|
@ -242,10 +239,9 @@ class TestAuthCLI:
|
|||
expected_msg = translation.format()
|
||||
assert expected_msg in str(exception)
|
||||
|
||||
mocker.patch("getpass.getpass", return_value="yoloswag")
|
||||
with pytest.raises(MoulinetteError) as exception:
|
||||
moulinette_cli.run(
|
||||
["testauth", "default"], output_as="none", password="yoloswag"
|
||||
)
|
||||
moulinette_cli.run(["testauth", "default"], output_as="none")
|
||||
|
||||
expected_msg = translation.format()
|
||||
assert expected_msg in str(exception)
|
||||
|
|
|
@ -371,6 +371,11 @@ def test_mkdir(tmp_path):
|
|||
|
||||
|
||||
def test_mkdir_with_permission(tmp_path, mocker):
|
||||
|
||||
# This test only make sense when not being root
|
||||
if os.getuid() == 0:
|
||||
return
|
||||
|
||||
new_path = tmp_path / "new_folder"
|
||||
permission = 0o700
|
||||
mkdir(str(new_path), mode=permission)
|
||||
|
|
|
@ -66,11 +66,14 @@ class TestLDAP:
|
|||
|
||||
assert ldap_interface.con
|
||||
|
||||
def test_authenticate_server_down(self, ldap_server):
|
||||
def test_authenticate_server_down(self, ldap_server, mocker):
|
||||
self.ldap_conf["parameters"]["uri"] = ldap_server.uri
|
||||
self.ldap_conf["parameters"]["user_rdn"] = "cn=admin,dc=yunohost,dc=org"
|
||||
ldap_server.stop()
|
||||
ldap_interface = m_ldap.Authenticator(**self.ldap_conf)
|
||||
|
||||
# Now if slapd is down, moulinette tries to restart it
|
||||
mocker.patch("os.system")
|
||||
with pytest.raises(MoulinetteError) as exception:
|
||||
ldap_interface.authenticate(password="yunohost")
|
||||
|
||||
|
@ -208,9 +211,10 @@ class TestLDAP:
|
|||
with pytest.raises(MoulinetteError) as exception:
|
||||
self.add_new_user(ldap_interface)
|
||||
|
||||
translation = m18n.g("ldap_operation_error", action="add")
|
||||
expected_msg = translation.format(action="add")
|
||||
assert expected_msg in str(exception)
|
||||
expected_message = "error during LDAP add operation with: rdn="
|
||||
expected_error = "modifications require authentication"
|
||||
assert expected_error in str(exception)
|
||||
assert expected_message in str(exception)
|
||||
|
||||
def remove_new_user(self, ldap_interface):
|
||||
new_user_info = self.add_new_user(
|
||||
|
@ -229,9 +233,10 @@ class TestLDAP:
|
|||
"uid=%s,ou=users,dc=yunohost,dc=org" % uid, attrs=None
|
||||
)
|
||||
|
||||
translation = m18n.g("ldap_operation_error", action="search")
|
||||
expected_msg = translation.format(action="search")
|
||||
assert expected_msg in str(exception)
|
||||
expected_message = "error during LDAP search operation with: base="
|
||||
expected_error = "No such object"
|
||||
assert expected_error in str(exception)
|
||||
assert expected_message in str(exception)
|
||||
|
||||
def test_admin_remove(self, ldap_server):
|
||||
self.ldap_conf["parameters"]["uri"] = ldap_server.uri
|
||||
|
@ -257,9 +262,10 @@ class TestLDAP:
|
|||
with pytest.raises(MoulinetteError) as exception:
|
||||
self.remove_new_user(ldap_interface)
|
||||
|
||||
translation = m18n.g("ldap_operation_error", action="remove")
|
||||
expected_msg = translation.format(action="remove")
|
||||
assert expected_msg in str(exception)
|
||||
expected_message = "error during LDAP delete operation with: rdn="
|
||||
expected_error = "modifications require authentication"
|
||||
assert expected_error in str(exception)
|
||||
assert expected_message in str(exception)
|
||||
|
||||
def update_new_user(self, ldap_interface, new_rdn=False):
|
||||
new_user_info = self.add_new_user(
|
||||
|
@ -336,9 +342,10 @@ class TestLDAP:
|
|||
with pytest.raises(MoulinetteError) as exception:
|
||||
self.update_new_user(ldap_interface)
|
||||
|
||||
translation = m18n.g("ldap_operation_error", action="update")
|
||||
expected_msg = translation.format(action="update")
|
||||
assert expected_msg in str(exception)
|
||||
expected_message = "error during LDAP update operation with: rdn="
|
||||
expected_error = "modifications require authentication"
|
||||
assert expected_error in str(exception)
|
||||
assert expected_message in str(exception)
|
||||
|
||||
def test_anonymous_update_new_rdn(self, ldap_server):
|
||||
self.ldap_conf["parameters"]["uri"] = ldap_server.uri
|
||||
|
@ -347,9 +354,10 @@ class TestLDAP:
|
|||
with pytest.raises(MoulinetteError) as exception:
|
||||
self.update_new_user(ldap_interface, True)
|
||||
|
||||
translation = m18n.g("ldap_operation_error", action="update")
|
||||
expected_msg = translation.format(action="update")
|
||||
assert expected_msg in str(exception)
|
||||
expected_message = "error during LDAP update operation with: rdn="
|
||||
expected_error = "modifications require authentication"
|
||||
assert expected_error in str(exception)
|
||||
assert expected_message in str(exception)
|
||||
|
||||
def test_empty_update(self, ldap_server):
|
||||
self.ldap_conf["parameters"]["uri"] = ldap_server.uri
|
||||
|
|
|
@ -115,6 +115,6 @@ def test_call_async_output_kwargs(test_file, mocker):
|
|||
|
||||
|
||||
def test_check_output(test_file):
|
||||
assert check_output(["cat", str(test_file)], shell=False) == "foo\nbar\n"
|
||||
assert check_output(["cat", str(test_file)], shell=False) == "foo\nbar"
|
||||
|
||||
assert check_output("cat %s" % str(test_file)) == "foo\nbar\n"
|
||||
assert check_output("cat %s" % str(test_file)) == "foo\nbar"
|
||||
|
|
30
tox.ini
30
tox.ini
|
@ -1,33 +1,21 @@
|
|||
[tox]
|
||||
envlist =
|
||||
py35
|
||||
lint
|
||||
envlist =
|
||||
py{27,3}-{pytest,lint}
|
||||
format
|
||||
format-check
|
||||
docs
|
||||
skipdist = True
|
||||
|
||||
[testenv]
|
||||
usedevelop = True
|
||||
passenv = *
|
||||
extras = tests
|
||||
deps =
|
||||
pytest >= 4.6.3, < 5.0
|
||||
pytest-cov >= 2.7.1, < 3.0
|
||||
pytest-mock >= 1.10.4, < 2.0
|
||||
pytest-env >= 0.6.2, < 1.0
|
||||
requests >= 2.22.0, < 3.0
|
||||
requests-mock >= 1.6.0, < 2.0
|
||||
toml >= 0.10, < 0.11
|
||||
gevent-websocket
|
||||
bottle >= 0.12
|
||||
WebTest >= 2.0, < 2.1
|
||||
py{27,3}-pytest: .[tests]
|
||||
py{27,3}-lint: flake8
|
||||
commands =
|
||||
pytest {posargs}
|
||||
|
||||
[testenv:lint]
|
||||
commands = flake8 moulinette test
|
||||
deps = flake8
|
||||
skip_install = True
|
||||
usedevelop = False
|
||||
|
||||
py{27,3}-pytest: pytest {posargs} -c pytest.ini
|
||||
py{27,3}-lint: flake8 moulinette test
|
||||
|
||||
[testenv:format]
|
||||
basepython = python3
|
||||
|
|
Loading…
Reference in a new issue