mirror of
https://github.com/YunoHost/moulinette.git
synced 2024-09-03 20:06:31 +02:00
Merge branch 'stretch-unstable' into sasl_authentication
This commit is contained in:
commit
ff6343c53a
33 changed files with 531 additions and 200 deletions
60
debian/changelog
vendored
60
debian/changelog
vendored
|
@ -1,3 +1,63 @@
|
|||
moulinette (3.5.2) stable; urgency=low
|
||||
|
||||
- Release as stable !
|
||||
- [fix] Do not miserably crash if the lock does not exist when attempting to release it
|
||||
- [i18n] Update translation for Arabic, Italian
|
||||
|
||||
Thanks to all contributors (Aleks, BoF, silkevicious) <3 !
|
||||
|
||||
-- Alexandre Aubin <alex.aubin@mailoo.org> Wed, 10 Apr 2019 02:14:00 +0000
|
||||
|
||||
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 !
|
||||
|
||||
-- Alexandre Aubin <alex.aubin@mailoo.org> Wed, 03 Apr 2019 02:25:00 +0000
|
||||
|
||||
moulinette (3.5.0) testing; urgency=low
|
||||
|
||||
* [i18n] Improve Russian and Chinese (Mandarin) translations
|
||||
|
||||
Contributors : n3uz, Алексей
|
||||
|
||||
-- Alexandre Aubin <alex.aubin@mailoo.org> Wed, 13 Mar 2019 17:20:00 +0000
|
||||
|
||||
moulinette (3.4.2) stable; urgency=low
|
||||
|
||||
* [i18n] Improve Basque translation
|
||||
|
||||
Thanks to all contributors (A. Garaialde) <3 !
|
||||
|
||||
-- Alexandre Aubin <alex.aubin@mailoo.org> Tue, 29 Jan 2019 16:50:00 +0000
|
||||
|
||||
moulinette (3.4.1) testing; urgency=low
|
||||
|
||||
* [i18n] Improve Chinese(Mandarin) translation
|
||||
* [i18n] Misc orthotypograhy
|
||||
|
||||
Thanks to all contributors (Jibec, aleiyer) <3 !
|
||||
|
||||
-- Alexandre Aubin <alex.aubin@mailoo.org> Thu, 17 Jan 2019 21:50:00 +0000
|
||||
|
||||
moulinette (3.4.0) testing; urgency=low
|
||||
|
||||
* [fix] Code cleaning with autopep8 (#187)
|
||||
* [fix] Automatically reconnect LDAP authenticator when slapd restarts (#185)
|
||||
* [enh] Display date as system timezone (#184)
|
||||
* [mod] Make sure `gpg.encrypt` actually does something (#182)
|
||||
* [mod] Add possiblity to get attribute name of conflict in LDAP (#181)
|
||||
* [enh] Simplify moulinette error management (#180)
|
||||
* [mod] Remove Access-Control-Allow-Origin (#174)
|
||||
* [enh] Protect against CSRF (#171)
|
||||
* [i18n] Improve Spanish translation
|
||||
|
||||
Thanks to all contributors (Aleks, randomstuff, irina11y, Josue, gdayon, ljf) <3 !
|
||||
|
||||
-- Alexandre Aubin <alex.aubin@mailoo.org> Thu, 20 Dec 2018 21:53:00 +0000
|
||||
|
||||
moulinette (3.3.1) stable; urgency=low
|
||||
|
||||
* [fix] 'force' semantics in 'utils.filesystem.mkdir' (#177)
|
||||
|
|
3
debian/control
vendored
3
debian/control
vendored
|
@ -16,7 +16,8 @@ Depends: ${misc:Depends}, ${python:Depends},
|
|||
python-gnupg,
|
||||
python-gevent-websocket,
|
||||
python-argcomplete,
|
||||
python-psutil
|
||||
python-psutil,
|
||||
python-tz
|
||||
Replaces: yunohost-cli
|
||||
Breaks: yunohost-cli
|
||||
Description: prototype interfaces with ease in Python
|
||||
|
|
|
@ -45,10 +45,11 @@
|
|||
"error_removing": "خطأ أثناء عملية حذف {path:s}: {error:s}",
|
||||
"error_changing_file_permissions": "خطأ أثناء عملية تعديل التصريحات لـ {path:s}: {error:s}",
|
||||
"invalid_url": "خطأ في عنوان الرابط {url:s} (هل هذا الموقع موجود حقًا ؟)",
|
||||
"download_ssl_error": "خطأ في الإتصال الآمن عبر الـ SSL أثناء محاولة الإتصال بـ {url:s}",
|
||||
"download_ssl_error": "خطأ في الاتصال الآمن عبر الـ SSL أثناء محاولة الربط بـ {url:s}",
|
||||
"download_timeout": "{url:s} استغرق مدة طويلة جدا للإستجابة، فتوقّف.",
|
||||
"download_unknown_error": "خطأ أثناء عملية تنزيل البيانات مِن {url:s} : {error:s}",
|
||||
"download_bad_status_code": "{url:s} أعاد رمز الحالة {code:s}",
|
||||
"command_unknown": "الأمر '{command:s}' غير معروف ؟",
|
||||
"corrupted_yaml": "قراءة مُشوّهة لنسق yaml مِن {ressource:s} (السبب : {error:s})"
|
||||
"corrupted_yaml": "قراءة مُشوّهة لنسق yaml مِن {ressource:s} (السبب : {error:s})",
|
||||
"info": "معلومة:"
|
||||
}
|
||||
|
|
1
locales/bn_BD.json
Normal file
1
locales/bn_BD.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
|
@ -50,5 +50,6 @@
|
|||
"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:"
|
||||
}
|
||||
|
|
54
locales/cmn.json
Normal file
54
locales/cmn.json
Normal file
|
@ -0,0 +1,54 @@
|
|||
{
|
||||
"argument_required": "{argument}是必须的",
|
||||
"authentication_profile_required": "必须验证配置文件{profile}",
|
||||
"authentication_required": "需要验证",
|
||||
"authentication_required_long": "此操作需要验证",
|
||||
"colon": "{} ",
|
||||
"confirm": "确认{prompt}",
|
||||
"deprecated_command": "{prog}{command}已经放弃使用,将来会删除",
|
||||
"deprecated_command_alias": "{prog}{old}已经放弃使用,将来会删除,请使用{prog}{new}代替",
|
||||
"error": "错误:",
|
||||
"error_see_log": "发生错误。请参看日志文件获取错误详情,日志文件位于 /var/log/yunohost/。",
|
||||
"file_exists": "文件已存在:{path}",
|
||||
"file_not_exist": "文件不存在:{path}",
|
||||
"folder_exists": "目录已存在:{path}",
|
||||
"folder_not_exist": "目录不存在",
|
||||
"info": "信息:",
|
||||
"instance_already_running": "实例已正在运行",
|
||||
"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_out": "注销成功",
|
||||
"not_logged_in": "您未登录",
|
||||
"operation_interrupted": "操作中断",
|
||||
"password": "密码",
|
||||
"pattern_not_match": "模式匹配失败",
|
||||
"root_required": "必须以root身份进行此操作",
|
||||
"server_already_running": "服务已运行在指定端口",
|
||||
"success": "成功!",
|
||||
"unable_authenticate": "认证失败",
|
||||
"unable_retrieve_session": "获取会话失败",
|
||||
"unknown_group": "未知组{group}",
|
||||
"unknown_user": "未知用户{user}",
|
||||
"values_mismatch": "值不匹配",
|
||||
"warning": "警告:",
|
||||
"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})",
|
||||
"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是否存在?)",
|
||||
"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}?"
|
||||
}
|
1
locales/el.json
Normal file
1
locales/el.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
|
@ -27,7 +27,6 @@
|
|||
"operation_interrupted": "Operation interrupted",
|
||||
"password": "Password",
|
||||
"pattern_not_match": "Does not match pattern",
|
||||
"permission_denied": "Permission denied",
|
||||
"root_required": "You must be root to perform this action",
|
||||
"server_already_running": "A server is already running on that port",
|
||||
"success": "Success!",
|
||||
|
@ -47,10 +46,10 @@
|
|||
"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_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?"
|
||||
}
|
||||
|
|
|
@ -49,5 +49,6 @@
|
|||
"download_timeout": "{url:s} tardó demasiado en responder, me rindo.",
|
||||
"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": "Comando '{command:s}' desconocido ?"
|
||||
"command_unknown": "Comando '{command:s}' desconocido ?",
|
||||
"corrupted_yaml": "yaml corrupto leido desde {ressource:s} (motivo: {error:s})"
|
||||
}
|
||||
|
|
3
locales/eu.json
Normal file
3
locales/eu.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"argument_required": "'{argument}' argumentua beharrezkoa da"
|
||||
}
|
|
@ -1,23 +1,23 @@
|
|||
{
|
||||
"argument_required": "L’argument « {argument} » est requis",
|
||||
"authentication_profile_required": "L’authentification au profil « {profile} » requise",
|
||||
"argument_required": "L’argument '{argument}' est requis",
|
||||
"authentication_profile_required": "L’authentification au profil '{profile}' est requise",
|
||||
"authentication_required": "Authentification requise",
|
||||
"authentication_required_long": "L’authentification est requise pour exécuter cette action",
|
||||
"colon": "{} : ",
|
||||
"confirm": "Confirmez : {prompt}",
|
||||
"deprecated_command": "« {prog} {command} » est déprécié et sera bientôt supprimé",
|
||||
"deprecated_command_alias": "« {prog} {old} » est déprécié et sera bientôt supprimé, utilisez « {prog} {new} » à la place",
|
||||
"deprecated_command": "'{prog} {command}' est déprécié et sera bientôt supprimé",
|
||||
"deprecated_command_alias": "'{prog} {old}' est déprécié et sera bientôt supprimé, utilisez '{prog} {new}' à la place",
|
||||
"error": "Erreur :",
|
||||
"error_see_log": "Une erreur est survenue. Veuillez consulter les journaux pour plus de détails, ils sont situés en /var/log/yunohost/.",
|
||||
"file_exists": "Le fichier existe déjà : « {path} »",
|
||||
"file_not_exist": "Le fichier « {path} » n’existe pas",
|
||||
"folder_exists": "Le dossier existe déjà : « {path} »",
|
||||
"error_see_log": "Une erreur est survenue. Veuillez consulter les journaux pour plus de détails, ils sont situés dans /var/log/yunohost/.",
|
||||
"file_exists": "Le fichier existe déjà : '{path}'",
|
||||
"file_not_exist": "Le fichier '{path}' n’existe pas",
|
||||
"folder_exists": "Le dossier existe déjà : '{path}'",
|
||||
"folder_not_exist": "Le dossier n’existe pas",
|
||||
"instance_already_running": "Une instance est déjà en cours d’exécution",
|
||||
"invalid_argument": "Argument « {argument} » incorrect : {error}",
|
||||
"invalid_argument": "Argument '{argument}' incorrect : {error}",
|
||||
"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 comme valeur : {value}",
|
||||
"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é",
|
||||
|
@ -32,23 +32,24 @@
|
|||
"success": "Succès !",
|
||||
"unable_authenticate": "Impossible de vous authentifier",
|
||||
"unable_retrieve_session": "Impossible de récupérer la session",
|
||||
"unknown_group": "Groupe « {group} » inconnu",
|
||||
"unknown_user": "Utilisateur « {user} » inconnu",
|
||||
"unknown_group": "Groupe '{group}' inconnu",
|
||||
"unknown_user": "L'utilisateur « {user} » est inconnu",
|
||||
"values_mismatch": "Les valeurs ne correspondent pas",
|
||||
"warning": "Attention :",
|
||||
"websocket_request_expected": "Requête WebSocket attendue",
|
||||
"cannot_open_file": "Impossible d’ouvrir le fichier {file:s} (cause : {error:s})",
|
||||
"cannot_write_file": "Ne peut pas écrire le fichier {file:s} (cause : {error:s})",
|
||||
"websocket_request_expected": "Une requête WebSocket est attendue",
|
||||
"cannot_open_file": "Impossible d’ouvrir le fichier {file:s} (raison : {error:s})",
|
||||
"cannot_write_file": "Ne peut pas écrire le fichier {file:s} (raison : {error:s})",
|
||||
"unknown_error_reading_file": "Erreur inconnue en essayant de lire le fichier {file:s}",
|
||||
"corrupted_json": "Json corrompu lu depuis {ressource:s} (cause : {error:s})",
|
||||
"error_writing_file": "Erreur en écrivant le fichier {file:s}:{error:s}",
|
||||
"error_removing": "Erreur lors de la suppression {path:s}:{error:s}",
|
||||
"error_changing_file_permissions": "Erreur lors de la modification des autorisations pour {path:s}:{error:s}",
|
||||
"invalid_url": "Url invalide {url:s} (ce site existe-t-il ?)",
|
||||
"corrupted_json": "Fichier JSON corrompu en lecture depuis {ressource:s} (raison : {error:s})",
|
||||
"error_writing_file": "Erreur en écrivant le fichier {file:s} : {error:s}",
|
||||
"error_removing": "Erreur lors de la suppression {path:s} : {error:s}",
|
||||
"error_changing_file_permissions": "Erreur lors de la modification des autorisations pour {path:s} : {error:s}",
|
||||
"invalid_url": "URL {url:s} invalide : ce site existe-t-il ?",
|
||||
"download_ssl_error": "Erreur SSL lors de la connexion à {url:s}",
|
||||
"download_timeout": "{url:s} a pris trop de temps pour répondre, abandon.",
|
||||
"download_unknown_error": "Erreur lors du téléchargement des données à partir de {url:s}:{error:s}",
|
||||
"download_bad_status_code": "{url:s} code de statut renvoyé {code:s}",
|
||||
"command_unknown": "Commande « {command:s} » inconnue ?",
|
||||
"corrupted_yaml": "YAML corrompu lu {ressource:s} depuis (cause : {error:s})"
|
||||
"download_timeout": "{url:s} a pris trop de temps pour répondre : abandon.",
|
||||
"download_unknown_error": "Erreur lors du téléchargement des données à partir de {url:s} : {error:s}",
|
||||
"download_bad_status_code": "{url:s} renvoie le code d'état {code:s}",
|
||||
"command_unknown": "Commande '{command:s}' inconnue ?",
|
||||
"corrupted_yaml": "Fichier YAML corrompu en lecture depuis {ressource:s} (raison : {error:s})",
|
||||
"info": "Info :"
|
||||
}
|
||||
|
|
1
locales/hu.json
Normal file
1
locales/hu.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
|
@ -50,5 +50,6 @@
|
|||
"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:"
|
||||
}
|
||||
|
|
|
@ -50,5 +50,6 @@
|
|||
"download_bad_status_code": "{url:s} tòrna lo còdi d’estat {code:s}",
|
||||
"command_unknown": "Comanda {command:s} desconeguda ?",
|
||||
"corrupted_json": "Fichièr Json corromput legit de {ressource:s} (rason : {error:s})",
|
||||
"corrupted_yaml": "Fichièr YAML corromput legit de {ressource:s} (rason : {error:s})"
|
||||
"corrupted_yaml": "Fichièr YAML corromput legit de {ressource:s} (rason : {error:s})",
|
||||
"info": "Info :"
|
||||
}
|
||||
|
|
1
locales/pl.json
Normal file
1
locales/pl.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
|
@ -1 +1,48 @@
|
|||
{}
|
||||
{
|
||||
"argument_required": "Требуется'{argument}' аргумент",
|
||||
"authentication_profile_required": "Для доступа к '{profile}' требуется аутентификация",
|
||||
"authentication_required": "Требуется аутентификация",
|
||||
"authentication_required_long": "Для этого действия требуется аутентификация",
|
||||
"colon": "{}: ",
|
||||
"confirm": "Подтвердить {prompt}",
|
||||
"deprecated_command": "'{prog} {command}' устарела и будет удалена",
|
||||
"deprecated_command_alias": "'{prog} {old}' устарела и будет удалена, вместо неё используйте '{prog} {new}'",
|
||||
"error": "Ошибка:",
|
||||
"error_see_log": "Произошла ошибка. Пожалуйста, смотри подробности в логах, находящихся /var/log/yunohost/.",
|
||||
"file_exists": "Файл уже существует: '{path}'",
|
||||
"file_not_exist": "Файл не существует: '{path}'",
|
||||
"folder_exists": "Каталог уже существует: '{path}'",
|
||||
"folder_not_exist": "Каталог не существует",
|
||||
"invalid_argument": "Неправильный аргумент '{argument}': {error}",
|
||||
"invalid_password": "Неправильный пароль",
|
||||
"ldap_attribute_already_exists": "Атрибут '{attribute}' уже существует со значением '{value}'",
|
||||
"logged_in": "Вы вошли",
|
||||
"logged_out": "Вы вышли из системы",
|
||||
"not_logged_in": "Вы не залогинились",
|
||||
"operation_interrupted": "Действие прервано",
|
||||
"password": "Пароль",
|
||||
"pattern_not_match": "Не соответствует образцу",
|
||||
"server_already_running": "Сервер уже запущен на этом порте",
|
||||
"success": "Отлично!",
|
||||
"unable_authenticate": "Аутентификация невозможна",
|
||||
"unknown_group": "Неизвестная '{group}' группа",
|
||||
"unknown_user": "Неизвестный '{user}' пользователь",
|
||||
"values_mismatch": "Неверные значения",
|
||||
"warning": "Внимание :",
|
||||
"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_yaml": "Повреждённой yaml получен от {ressource:s} (причина: {error:s})",
|
||||
"error_writing_file": "Ошибка при записи файла {file:s}: {error:s}",
|
||||
"error_removing": "Ошибка при удалении {path:s}: {error:s}",
|
||||
"invalid_url": "Неправильный url {url:s} (этот сайт существует ?)",
|
||||
"download_ssl_error": "Ошибка SSL при соединении с {url:s}",
|
||||
"download_timeout": "Превышено время ожидания ответа от {url:s}.",
|
||||
"download_unknown_error": "Ошибка при загрузке данных с {url:s} : {error:s}",
|
||||
"instance_already_running": "Процесс уже запущен",
|
||||
"ldap_operation_error": "Ошибка в процессе работы LDAP",
|
||||
"root_required": "Чтобы выполнить это действие, вы должны иметь права root",
|
||||
"corrupted_json": "Повреждённый json получен от {ressource:s} (причина: {error:s})",
|
||||
"command_unknown": "Команда '{command:s}' неизвестна ?"
|
||||
}
|
||||
|
|
1
locales/sv.json
Normal file
1
locales/sv.json
Normal file
|
@ -0,0 +1 @@
|
|||
{}
|
|
@ -137,5 +137,5 @@ def cli(namespaces, args, use_cache=True, output_as=None,
|
|||
except MoulinetteError as e:
|
||||
import logging
|
||||
logging.getLogger(namespaces[0]).error(e.strerror)
|
||||
return e.errno
|
||||
return 1
|
||||
return 0
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
import os
|
||||
import re
|
||||
import errno
|
||||
import logging
|
||||
import yaml
|
||||
import cPickle as pickle
|
||||
|
@ -26,6 +25,7 @@ logger = logging.getLogger('moulinette.actionsmap')
|
|||
# Extra parameters definition
|
||||
|
||||
class _ExtraParameter(object):
|
||||
|
||||
"""
|
||||
Argument parser for an extra parameter.
|
||||
|
||||
|
@ -105,6 +105,7 @@ class CommentParameter(_ExtraParameter):
|
|||
|
||||
|
||||
class AskParameter(_ExtraParameter):
|
||||
|
||||
"""
|
||||
Ask for the argument value if possible and needed.
|
||||
|
||||
|
@ -139,6 +140,7 @@ class AskParameter(_ExtraParameter):
|
|||
|
||||
|
||||
class PasswordParameter(AskParameter):
|
||||
|
||||
"""
|
||||
Ask for the password argument value if possible and needed.
|
||||
|
||||
|
@ -160,6 +162,7 @@ class PasswordParameter(AskParameter):
|
|||
|
||||
|
||||
class PatternParameter(_ExtraParameter):
|
||||
|
||||
"""
|
||||
Check if the argument value match a pattern.
|
||||
|
||||
|
@ -187,9 +190,8 @@ class PatternParameter(_ExtraParameter):
|
|||
if msg == message:
|
||||
msg = m18n.g(message)
|
||||
|
||||
raise MoulinetteError(errno.EINVAL,
|
||||
m18n.g('invalid_argument',
|
||||
argument=arg_name, error=msg))
|
||||
raise MoulinetteError('invalid_argument',
|
||||
argument=arg_name, error=msg)
|
||||
return arg_value
|
||||
|
||||
@staticmethod
|
||||
|
@ -206,6 +208,7 @@ class PatternParameter(_ExtraParameter):
|
|||
|
||||
|
||||
class RequiredParameter(_ExtraParameter):
|
||||
|
||||
"""
|
||||
Check if a required argument is defined or not.
|
||||
|
||||
|
@ -218,9 +221,8 @@ class RequiredParameter(_ExtraParameter):
|
|||
if required and (arg_value is None or arg_value == ''):
|
||||
logger.debug("argument '%s' is required",
|
||||
arg_name)
|
||||
raise MoulinetteError(errno.EINVAL,
|
||||
m18n.g('argument_required',
|
||||
argument=arg_name))
|
||||
raise MoulinetteError('argument_required',
|
||||
argument=arg_name)
|
||||
return arg_value
|
||||
|
||||
@staticmethod
|
||||
|
@ -243,6 +245,7 @@ extraparameters_list = [CommentParameter, AskParameter, PasswordParameter,
|
|||
|
||||
|
||||
class ExtraArgumentParser(object):
|
||||
|
||||
"""
|
||||
Argument validator and parser for the extra parameters.
|
||||
|
||||
|
@ -285,7 +288,7 @@ class ExtraArgumentParser(object):
|
|||
except Exception as e:
|
||||
logger.error("unable to validate extra parameter '%s' "
|
||||
"for argument '%s': %s", p, arg_name, e)
|
||||
raise MoulinetteError(errno.EINVAL, m18n.g('error_see_log'))
|
||||
raise MoulinetteError('error_see_log')
|
||||
|
||||
return parameters
|
||||
|
||||
|
@ -360,6 +363,7 @@ def ordered_yaml_load(stream):
|
|||
|
||||
|
||||
class ActionsMap(object):
|
||||
|
||||
"""Validate and process actions defined into an actions map
|
||||
|
||||
The actions map defines the features - and their usage - of an
|
||||
|
@ -501,7 +505,7 @@ class ActionsMap(object):
|
|||
except (AttributeError, ImportError):
|
||||
logger.exception("unable to load function %s.%s",
|
||||
namespace, func_name)
|
||||
raise MoulinetteError(errno.EIO, m18n.g('error_see_log'))
|
||||
raise MoulinetteError('error_see_log')
|
||||
else:
|
||||
log_id = start_action_logging()
|
||||
if logger.isEnabledFor(logging.DEBUG):
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import errno
|
||||
import gnupg
|
||||
import logging
|
||||
|
||||
from moulinette import m18n
|
||||
from moulinette.cache import open_cachefile
|
||||
from moulinette.core import MoulinetteError
|
||||
|
||||
|
@ -14,6 +12,7 @@ logger = logging.getLogger('moulinette.authenticator')
|
|||
# Base Class -----------------------------------------------------------
|
||||
|
||||
class BaseAuthenticator(object):
|
||||
|
||||
"""Authenticator base representation
|
||||
|
||||
Each authenticators must implement an Authenticator class derived
|
||||
|
@ -97,7 +96,7 @@ class BaseAuthenticator(object):
|
|||
except TypeError:
|
||||
logger.error("unable to extract token parts from '%s'", token)
|
||||
if password is None:
|
||||
raise MoulinetteError(errno.EINVAL, m18n.g('error_see_log'))
|
||||
raise MoulinetteError('error_see_log')
|
||||
|
||||
logger.info("session will not be stored")
|
||||
store_session = False
|
||||
|
@ -114,7 +113,7 @@ class BaseAuthenticator(object):
|
|||
except:
|
||||
logger.exception("authentication (name: '%s', vendor: '%s') fails",
|
||||
self.name, self.vendor)
|
||||
raise MoulinetteError(errno.EACCES, m18n.g('unable_authenticate'))
|
||||
raise MoulinetteError('unable_authenticate')
|
||||
|
||||
# Store session
|
||||
if store_session:
|
||||
|
@ -138,9 +137,13 @@ class BaseAuthenticator(object):
|
|||
"""Store a session and its associated password"""
|
||||
gpg = gnupg.GPG()
|
||||
gpg.encoding = 'utf-8'
|
||||
|
||||
# Encrypt the password using the session hash
|
||||
s = str(gpg.encrypt(password, None, symmetric=True, passphrase=session_hash))
|
||||
assert len(s), "For some reason GPG can't perform encryption, maybe check /root/.gnupg/gpg.conf or re-run with gpg = gnupg.GPG(verbose=True) ?"
|
||||
|
||||
with self._open_sessionfile(session_id, 'w') as f:
|
||||
f.write(str(gpg.encrypt(password, None, symmetric=True,
|
||||
passphrase=session_hash)))
|
||||
f.write(s)
|
||||
|
||||
def _retrieve_session(self, session_id, session_hash):
|
||||
"""Retrieve a session and return its associated password"""
|
||||
|
@ -149,8 +152,7 @@ class BaseAuthenticator(object):
|
|||
enc_pwd = f.read()
|
||||
except IOError:
|
||||
logger.debug("unable to retrieve session", exc_info=1)
|
||||
raise MoulinetteError(errno.ENOENT,
|
||||
m18n.g('unable_retrieve_session'))
|
||||
raise MoulinetteError('unable_retrieve_session')
|
||||
else:
|
||||
gpg = gnupg.GPG()
|
||||
gpg.encoding = 'utf-8'
|
||||
|
@ -159,6 +161,5 @@ class BaseAuthenticator(object):
|
|||
if decrypted.ok is not True:
|
||||
logger.error("unable to decrypt password for the session: %s",
|
||||
decrypted.status)
|
||||
raise MoulinetteError(errno.EINVAL,
|
||||
m18n.g('unable_retrieve_session'))
|
||||
raise MoulinetteError('unable_retrieve_session')
|
||||
return decrypted.data
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
|
||||
# TODO: Use Python3 to remove this fix!
|
||||
from __future__ import absolute_import
|
||||
import errno
|
||||
import logging
|
||||
import random
|
||||
import string
|
||||
|
@ -11,7 +10,6 @@ import ldap
|
|||
import ldap.sasl
|
||||
import ldap.modlist as modlist
|
||||
|
||||
from moulinette import m18n
|
||||
from moulinette.core import MoulinetteError
|
||||
from moulinette.authenticators import BaseAuthenticator
|
||||
|
||||
|
@ -21,6 +19,7 @@ logger = logging.getLogger('moulinette.authenticator.ldap')
|
|||
# LDAP Class Implementation --------------------------------------------
|
||||
|
||||
class Authenticator(BaseAuthenticator):
|
||||
|
||||
"""LDAP Authenticator
|
||||
|
||||
Initialize a LDAP connexion for the given arguments. It attempts to
|
||||
|
@ -80,7 +79,7 @@ class Authenticator(BaseAuthenticator):
|
|||
|
||||
def authenticate(self, password):
|
||||
try:
|
||||
con = ldap.initialize(self.uri)
|
||||
con = ldap.ldapobject.ReconnectLDAPObject(self.uri, retry_max=10, retry_delay=0.5)
|
||||
if self.userdn:
|
||||
if 'cn=external,cn=auth' in self.userdn:
|
||||
con.sasl_non_interactive_bind_s('EXTERNAL')
|
||||
|
@ -89,10 +88,10 @@ class Authenticator(BaseAuthenticator):
|
|||
else:
|
||||
con.simple_bind_s()
|
||||
except ldap.INVALID_CREDENTIALS:
|
||||
raise MoulinetteError(errno.EACCES, m18n.g('invalid_password'))
|
||||
raise MoulinetteError('invalid_password')
|
||||
except ldap.SERVER_DOWN:
|
||||
logger.exception('unable to reach the server to authenticate')
|
||||
raise MoulinetteError(169, m18n.g('ldap_server_down'))
|
||||
raise MoulinetteError('ldap_server_down')
|
||||
else:
|
||||
self.con = con
|
||||
self._ensure_password_uses_strong_hash(password)
|
||||
|
@ -144,7 +143,7 @@ class Authenticator(BaseAuthenticator):
|
|||
except Exception as e:
|
||||
logger.exception("error during LDAP search operation with: base='%s', "
|
||||
"filter='%s', attrs=%s and exception %s", base, filter, attrs, e)
|
||||
raise MoulinetteError(169, m18n.g('ldap_operation_error'))
|
||||
raise MoulinetteError('ldap_operation_error')
|
||||
|
||||
result_list = []
|
||||
if not attrs or 'dn' not in attrs:
|
||||
|
@ -175,7 +174,7 @@ class Authenticator(BaseAuthenticator):
|
|||
except Exception as e:
|
||||
logger.exception("error during LDAP add operation with: rdn='%s', "
|
||||
"attr_dict=%s and exception %s", rdn, attr_dict, e)
|
||||
raise MoulinetteError(169, m18n.g('ldap_operation_error'))
|
||||
raise MoulinetteError('ldap_operation_error')
|
||||
else:
|
||||
return True
|
||||
|
||||
|
@ -195,7 +194,7 @@ class Authenticator(BaseAuthenticator):
|
|||
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(169, m18n.g('ldap_operation_error'))
|
||||
raise MoulinetteError('ldap_operation_error')
|
||||
else:
|
||||
return True
|
||||
|
||||
|
@ -226,7 +225,7 @@ class Authenticator(BaseAuthenticator):
|
|||
logger.exception("error during LDAP update operation with: rdn='%s', "
|
||||
"attr_dict=%s, new_rdn=%s and exception: %s", rdn, attr_dict,
|
||||
new_rdn, e)
|
||||
raise MoulinetteError(169, m18n.g('ldap_operation_error'))
|
||||
raise MoulinetteError('ldap_operation_error')
|
||||
else:
|
||||
return True
|
||||
|
||||
|
@ -240,14 +239,30 @@ class Authenticator(BaseAuthenticator):
|
|||
Returns:
|
||||
Boolean | MoulinetteError
|
||||
|
||||
"""
|
||||
attr_found = self.get_conflict(value_dict)
|
||||
if attr_found:
|
||||
logger.info("attribute '%s' with value '%s' is not unique",
|
||||
attr_found[0], attr_found[1])
|
||||
raise MoulinetteError('ldap_attribute_already_exists',
|
||||
attribute=attr_found[0],
|
||||
value=attr_found[1])
|
||||
return True
|
||||
|
||||
def get_conflict(self, value_dict, base_dn=None):
|
||||
"""
|
||||
Check uniqueness of values
|
||||
|
||||
Keyword arguments:
|
||||
value_dict -- Dictionnary of attributes/values to check
|
||||
|
||||
Returns:
|
||||
None | list with Fist conflict attribute name and value
|
||||
|
||||
"""
|
||||
for attr, value in value_dict.items():
|
||||
if not self.search(filter=attr + '=' + value):
|
||||
if not self.search(base=base_dn, filter=attr + '=' + value):
|
||||
continue
|
||||
else:
|
||||
logger.info("attribute '%s' with value '%s' is not unique",
|
||||
attr, value)
|
||||
raise MoulinetteError(errno.EEXIST,
|
||||
m18n.g('ldap_attribute_already_exists',
|
||||
attribute=attr, value=value))
|
||||
return True
|
||||
return (attr, value)
|
||||
return None
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
import os
|
||||
import time
|
||||
import json
|
||||
import errno
|
||||
import logging
|
||||
import psutil
|
||||
|
||||
|
@ -20,6 +19,7 @@ logger = logging.getLogger('moulinette.core')
|
|||
# Internationalization -------------------------------------------------
|
||||
|
||||
class Translator(object):
|
||||
|
||||
"""Internationalization class
|
||||
|
||||
Provide an internationalization mechanism based on JSON files to
|
||||
|
@ -138,6 +138,7 @@ class Translator(object):
|
|||
|
||||
|
||||
class Moulinette18n(object):
|
||||
|
||||
"""Internationalization service for the moulinette
|
||||
|
||||
Manage internationalization and access to the proper keys translation
|
||||
|
@ -215,6 +216,7 @@ class Moulinette18n(object):
|
|||
|
||||
|
||||
class MoulinetteSignals(object):
|
||||
|
||||
"""Signals connector for the moulinette
|
||||
|
||||
Allow to easily connect signals from the moulinette to handlers. A
|
||||
|
@ -344,7 +346,7 @@ def init_interface(name, kwargs={}, actionsmap={}):
|
|||
mod = import_module('moulinette.interfaces.%s' % name)
|
||||
except ImportError:
|
||||
logger.exception("unable to load interface '%s'", name)
|
||||
raise MoulinetteError(errno.EINVAL, moulinette.m18n.g('error_see_log'))
|
||||
raise MoulinetteError('error_see_log')
|
||||
else:
|
||||
try:
|
||||
# Retrieve interface classes
|
||||
|
@ -352,7 +354,7 @@ def init_interface(name, kwargs={}, actionsmap={}):
|
|||
interface = mod.Interface
|
||||
except AttributeError:
|
||||
logger.exception("unable to retrieve classes of interface '%s'", name)
|
||||
raise MoulinetteError(errno.EIO, moulinette.m18n.g('error_see_log'))
|
||||
raise MoulinetteError('error_see_log')
|
||||
|
||||
# Instantiate or retrieve ActionsMap
|
||||
if isinstance(actionsmap, dict):
|
||||
|
@ -361,12 +363,12 @@ def init_interface(name, kwargs={}, actionsmap={}):
|
|||
amap = actionsmap
|
||||
else:
|
||||
logger.error("invalid actionsmap value %r", actionsmap)
|
||||
raise MoulinetteError(errno.EINVAL, moulinette.m18n.g('error_see_log'))
|
||||
raise MoulinetteError('error_see_log')
|
||||
|
||||
return interface(amap, **kwargs)
|
||||
|
||||
|
||||
def init_authenticator((vendor, name), kwargs={}):
|
||||
def init_authenticator(vendor_and_name, kwargs={}):
|
||||
"""Return a new authenticator instance
|
||||
|
||||
Retrieve the given authenticator vendor and return a new instance of
|
||||
|
@ -378,11 +380,12 @@ def init_authenticator((vendor, name), kwargs={}):
|
|||
- kwargs -- A dict of arguments for the authenticator profile
|
||||
|
||||
"""
|
||||
(vendor, name) = vendor_and_name
|
||||
try:
|
||||
mod = import_module('moulinette.authenticators.%s' % vendor)
|
||||
except ImportError:
|
||||
logger.exception("unable to load authenticator vendor '%s'", vendor)
|
||||
raise MoulinetteError(errno.EINVAL, moulinette.m18n.g('error_see_log'))
|
||||
raise MoulinetteError('error_see_log')
|
||||
else:
|
||||
return mod.Authenticator(name, **kwargs)
|
||||
|
||||
|
@ -411,12 +414,21 @@ def clean_session(session_id, profiles=[]):
|
|||
|
||||
# Moulinette core classes ----------------------------------------------
|
||||
|
||||
class MoulinetteError(OSError):
|
||||
class MoulinetteError(Exception):
|
||||
|
||||
"""Moulinette base exception"""
|
||||
pass
|
||||
|
||||
def __init__(self, key, raw_msg=False, *args, **kwargs):
|
||||
if raw_msg:
|
||||
msg = key
|
||||
else:
|
||||
msg = moulinette.m18n.g(key, *args, **kwargs)
|
||||
super(MoulinetteError, self).__init__(msg)
|
||||
self.strerror = msg
|
||||
|
||||
|
||||
class MoulinetteLock(object):
|
||||
|
||||
"""Locker for a moulinette instance
|
||||
|
||||
It provides a lock mechanism for a given moulinette instance. It can
|
||||
|
@ -471,8 +483,7 @@ class MoulinetteLock(object):
|
|||
break
|
||||
|
||||
if self.timeout is not None and (time.time() - start_time) > self.timeout:
|
||||
raise MoulinetteError(errno.EBUSY,
|
||||
moulinette.m18n.g('instance_already_running'))
|
||||
raise MoulinetteError('instance_already_running')
|
||||
# Wait before checking again
|
||||
time.sleep(self.interval)
|
||||
|
||||
|
@ -486,7 +497,10 @@ class MoulinetteLock(object):
|
|||
|
||||
"""
|
||||
if self._locked:
|
||||
os.unlink(self._lockfile)
|
||||
if os.path.exists(self._lockfile):
|
||||
os.unlink(self._lockfile)
|
||||
else:
|
||||
logger.warning("Uhoh, somehow the lock %s did not exist ..." % self._lockfile)
|
||||
logger.debug('lock has been released')
|
||||
self._locked = False
|
||||
|
||||
|
@ -495,10 +509,7 @@ class MoulinetteLock(object):
|
|||
with open(self._lockfile, 'w') as f:
|
||||
f.write(str(os.getpid()))
|
||||
except IOError:
|
||||
raise MoulinetteError(
|
||||
errno.EPERM, '%s. %s.'.format(
|
||||
moulinette.m18n.g('permission_denied'),
|
||||
moulinette.m18n.g('root_required')))
|
||||
raise MoulinetteError('root_required')
|
||||
|
||||
def _lock_PIDs(self):
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
import re
|
||||
import errno
|
||||
import logging
|
||||
import argparse
|
||||
import copy
|
||||
|
@ -20,6 +19,7 @@ CALLBACKS_PROP = '_callbacks'
|
|||
# Base Class -----------------------------------------------------------
|
||||
|
||||
class BaseActionsMapParser(object):
|
||||
|
||||
"""Actions map's base Parser
|
||||
|
||||
Each interfaces must implement an ActionsMapParser class derived
|
||||
|
@ -142,7 +142,7 @@ class BaseActionsMapParser(object):
|
|||
# Validate tid and namespace
|
||||
if not isinstance(tid, tuple) and \
|
||||
(namespace is None or not hasattr(namespace, TO_RETURN_PROP)):
|
||||
raise MoulinetteError(errno.EINVAL, m18n.g('invalid_usage'))
|
||||
raise MoulinetteError('invalid_usage')
|
||||
elif not tid:
|
||||
tid = GLOBAL_SECTION
|
||||
|
||||
|
@ -158,8 +158,7 @@ class BaseActionsMapParser(object):
|
|||
# TODO: Catch errors
|
||||
auth = msignals.authenticate(cls(), **auth_conf)
|
||||
if not auth.is_authenticated:
|
||||
raise MoulinetteError(errno.EACCES,
|
||||
m18n.g('authentication_required_long'))
|
||||
raise MoulinetteError('authentication_required_long')
|
||||
if self.get_conf(tid, 'argument_auth') and \
|
||||
self.get_conf(tid, 'authenticate') == 'all':
|
||||
namespace.auth = auth
|
||||
|
@ -263,7 +262,7 @@ class BaseActionsMapParser(object):
|
|||
else:
|
||||
logger.error("expecting 'all', 'False' or a list for "
|
||||
"configuration 'authenticate', got %r", ifaces)
|
||||
raise MoulinetteError(errno.EINVAL, m18n.g('error_see_log'))
|
||||
raise MoulinetteError('error_see_log')
|
||||
|
||||
# -- 'authenticator'
|
||||
try:
|
||||
|
@ -278,7 +277,7 @@ class BaseActionsMapParser(object):
|
|||
except KeyError:
|
||||
logger.error("requesting profile '%s' which is undefined in "
|
||||
"global configuration of 'authenticator'", auth)
|
||||
raise MoulinetteError(errno.EINVAL, m18n.g('error_see_log'))
|
||||
raise MoulinetteError('error_see_log')
|
||||
elif is_global and isinstance(auth, dict):
|
||||
if len(auth) == 0:
|
||||
logger.warning('no profile defined in global configuration '
|
||||
|
@ -301,7 +300,7 @@ class BaseActionsMapParser(object):
|
|||
else:
|
||||
logger.error("expecting a dict of profile(s) or a profile name "
|
||||
"for configuration 'authenticator', got %r", auth)
|
||||
raise MoulinetteError(errno.EINVAL, m18n.g('error_see_log'))
|
||||
raise MoulinetteError('error_see_log')
|
||||
|
||||
# -- 'argument_auth'
|
||||
try:
|
||||
|
@ -314,7 +313,7 @@ class BaseActionsMapParser(object):
|
|||
else:
|
||||
logger.error("expecting a boolean for configuration "
|
||||
"'argument_auth', got %r", arg_auth)
|
||||
raise MoulinetteError(errno.EINVAL, m18n.g('error_see_log'))
|
||||
raise MoulinetteError('error_see_log')
|
||||
|
||||
# -- 'lock'
|
||||
try:
|
||||
|
@ -327,7 +326,7 @@ class BaseActionsMapParser(object):
|
|||
else:
|
||||
logger.error("expecting a boolean for configuration 'lock', "
|
||||
"got %r", lock)
|
||||
raise MoulinetteError(errno.EINVAL, m18n.g('error_see_log'))
|
||||
raise MoulinetteError('error_see_log')
|
||||
|
||||
return conf
|
||||
|
||||
|
@ -354,6 +353,7 @@ class BaseActionsMapParser(object):
|
|||
|
||||
|
||||
class BaseInterface(object):
|
||||
|
||||
"""Moulinette's base Interface
|
||||
|
||||
Each interfaces must implement an Interface class derived from this
|
||||
|
@ -426,8 +426,8 @@ class _CallbackAction(argparse.Action):
|
|||
value = self.callback(namespace, values, **self.callback_kwargs)
|
||||
except:
|
||||
logger.exception("cannot get value from callback method "
|
||||
"'{0}'".format(self.callback_method))
|
||||
raise MoulinetteError(errno.EINVAL, m18n.g('error_see_log'))
|
||||
"'{0}'".format(self.callback_method))
|
||||
raise MoulinetteError('error_see_log')
|
||||
else:
|
||||
if value:
|
||||
if self.callback_return:
|
||||
|
@ -437,6 +437,7 @@ class _CallbackAction(argparse.Action):
|
|||
|
||||
|
||||
class _ExtendedSubParsersAction(argparse._SubParsersAction):
|
||||
|
||||
"""Subparsers with extended properties for argparse
|
||||
|
||||
It provides the following additional properties at initialization,
|
||||
|
|
|
@ -11,6 +11,7 @@ from gevent.queue import Queue
|
|||
from geventwebsocket import WebSocketError
|
||||
|
||||
from bottle import run, request, response, Bottle, HTTPResponse
|
||||
from bottle import get, post, install, abort, delete, put
|
||||
|
||||
from moulinette import msignals, m18n, DATA_DIR
|
||||
from moulinette.core import MoulinetteError, clean_session
|
||||
|
@ -26,12 +27,43 @@ logger = log.getLogger('moulinette.interface.api')
|
|||
|
||||
# API helpers ----------------------------------------------------------
|
||||
|
||||
CSRF_TYPES = set(["text/plain",
|
||||
"application/x-www-form-urlencoded",
|
||||
"multipart/form-data"])
|
||||
|
||||
|
||||
def is_csrf():
|
||||
"""Checks is this is a CSRF request."""
|
||||
|
||||
if request.method != "POST":
|
||||
return False
|
||||
if request.content_type is None:
|
||||
return True
|
||||
content_type = request.content_type.lower().split(';')[0]
|
||||
if content_type not in CSRF_TYPES:
|
||||
return False
|
||||
|
||||
return request.headers.get("X-Requested-With") is None
|
||||
|
||||
|
||||
# Protection against CSRF
|
||||
def filter_csrf(callback):
|
||||
def wrapper(*args, **kwargs):
|
||||
if is_csrf():
|
||||
abort(403, "CSRF protection")
|
||||
else:
|
||||
return callback(*args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
class LogQueues(dict):
|
||||
|
||||
"""Map of session id to queue."""
|
||||
pass
|
||||
|
||||
|
||||
class APIQueueHandler(logging.Handler):
|
||||
|
||||
"""
|
||||
A handler class which store logging records into a queue, to be used
|
||||
and retrieved from the API.
|
||||
|
@ -57,6 +89,7 @@ class APIQueueHandler(logging.Handler):
|
|||
|
||||
|
||||
class _HTTPArgumentParser(object):
|
||||
|
||||
"""Argument parser for HTTP requests
|
||||
|
||||
Object for parsing HTTP requests into Python objects. It is based
|
||||
|
@ -159,10 +192,11 @@ class _HTTPArgumentParser(object):
|
|||
|
||||
def _error(self, message):
|
||||
# TODO: Raise a proper exception
|
||||
raise MoulinetteError(1, message)
|
||||
raise MoulinetteError(message)
|
||||
|
||||
|
||||
class _ActionsMapPlugin(object):
|
||||
|
||||
"""Actions map Bottle Plugin
|
||||
|
||||
Process relevant action for the request using the actions map and
|
||||
|
@ -317,7 +351,7 @@ class _ActionsMapPlugin(object):
|
|||
self.logout(profile)
|
||||
except:
|
||||
pass
|
||||
raise error_to_response(e)
|
||||
raise HTTPUnauthorizedResponse(e.strerror)
|
||||
else:
|
||||
# Update dicts with new values
|
||||
s_hashes[profile] = s_hash
|
||||
|
@ -404,7 +438,7 @@ class _ActionsMapPlugin(object):
|
|||
try:
|
||||
ret = self.actionsmap.process(arguments, timeout=30, route=_route)
|
||||
except MoulinetteError as e:
|
||||
raise error_to_response(e)
|
||||
raise HTTPBadRequestResponse(e.strerror)
|
||||
except Exception as e:
|
||||
if isinstance(e, HTTPResponse):
|
||||
raise e
|
||||
|
@ -488,38 +522,12 @@ class HTTPUnauthorizedResponse(HTTPResponse):
|
|||
super(HTTPUnauthorizedResponse, self).__init__(output, 401)
|
||||
|
||||
|
||||
class HTTPForbiddenResponse(HTTPResponse):
|
||||
|
||||
def __init__(self, output=''):
|
||||
super(HTTPForbiddenResponse, self).__init__(output, 403)
|
||||
|
||||
|
||||
class HTTPErrorResponse(HTTPResponse):
|
||||
|
||||
def __init__(self, output=''):
|
||||
super(HTTPErrorResponse, self).__init__(output, 500)
|
||||
|
||||
|
||||
def error_to_response(error):
|
||||
"""Convert a MoulinetteError to relevant HTTP response."""
|
||||
if error.errno == errno.EPERM:
|
||||
return HTTPForbiddenResponse(error.strerror)
|
||||
elif error.errno == errno.EACCES:
|
||||
return HTTPUnauthorizedResponse(error.strerror)
|
||||
# Client-side error
|
||||
elif error.errno in [errno.ENOENT, errno.ESRCH, errno.ENXIO, errno.EEXIST,
|
||||
errno.ENODEV, errno.EINVAL, errno.ENOPKG, errno.EDESTADDRREQ]:
|
||||
return HTTPBadRequestResponse(error.strerror)
|
||||
# Server-side error
|
||||
elif error.errno in [errno.EIO, errno.EBUSY, errno.ENODATA, errno.EINTR,
|
||||
errno.ENETUNREACH]:
|
||||
return HTTPErrorResponse(error.strerror)
|
||||
else:
|
||||
logger.debug('unknown relevant response for error [%s] %s',
|
||||
error.errno, error.strerror)
|
||||
return HTTPErrorResponse(error.strerror)
|
||||
|
||||
|
||||
def format_for_response(content):
|
||||
"""Format the resulted content of a request for the HTTP response."""
|
||||
if request.method == 'POST':
|
||||
|
@ -541,6 +549,7 @@ def format_for_response(content):
|
|||
# API Classes Implementation -------------------------------------------
|
||||
|
||||
class ActionsMapParser(BaseActionsMapParser):
|
||||
|
||||
"""Actions map's Parser for the API
|
||||
|
||||
Provide actions map parsing methods for a CLI usage. The parser for
|
||||
|
@ -630,7 +639,7 @@ class ActionsMapParser(BaseActionsMapParser):
|
|||
tid, parser = self._parsers[route]
|
||||
except KeyError:
|
||||
logger.error("no argument parser found for route '%s'", route)
|
||||
raise MoulinetteError(errno.EINVAL, m18n.g('error_see_log'))
|
||||
raise MoulinetteError('error_see_log')
|
||||
ret = argparse.Namespace()
|
||||
|
||||
# Perform authentication if needed
|
||||
|
@ -643,7 +652,7 @@ class ActionsMapParser(BaseActionsMapParser):
|
|||
# TODO: Catch errors
|
||||
auth = msignals.authenticate(klass(), **auth_conf)
|
||||
if not auth.is_authenticated:
|
||||
raise MoulinetteError(errno.EACCES, m18n.g('authentication_required_long'))
|
||||
raise MoulinetteError('authentication_required_long')
|
||||
if self.get_conf(tid, 'argument_auth') and \
|
||||
self.get_conf(tid, 'authenticate') == 'all':
|
||||
ret.auth = auth
|
||||
|
@ -677,6 +686,7 @@ class ActionsMapParser(BaseActionsMapParser):
|
|||
|
||||
|
||||
class Interface(BaseInterface):
|
||||
|
||||
"""Application Programming Interface for the moulinette
|
||||
|
||||
Initialize a HTTP server which serves the API connected to a given
|
||||
|
@ -722,6 +732,7 @@ class Interface(BaseInterface):
|
|||
return callback
|
||||
|
||||
# Install plugins
|
||||
app.install(filter_csrf)
|
||||
app.install(apiheader)
|
||||
app.install(api18n)
|
||||
app.install(_ActionsMapPlugin(actionsmap, use_websocket, log_queues))
|
||||
|
@ -765,9 +776,8 @@ class Interface(BaseInterface):
|
|||
logger.exception("unable to start the server instance on %s:%d",
|
||||
host, port)
|
||||
if e.args[0] == errno.EADDRINUSE:
|
||||
raise MoulinetteError(errno.EADDRINUSE,
|
||||
m18n.g('server_already_running'))
|
||||
raise MoulinetteError(errno.EIO, m18n.g('error_see_log'))
|
||||
raise MoulinetteError('server_already_running')
|
||||
raise MoulinetteError('error_see_log')
|
||||
|
||||
# Routes handlers
|
||||
|
||||
|
|
|
@ -2,12 +2,14 @@
|
|||
|
||||
import os
|
||||
import sys
|
||||
import errno
|
||||
import getpass
|
||||
import locale
|
||||
import logging
|
||||
from argparse import SUPPRESS
|
||||
from collections import OrderedDict
|
||||
import time
|
||||
import pytz
|
||||
from datetime import date, datetime
|
||||
|
||||
import argcomplete
|
||||
|
||||
|
@ -94,6 +96,32 @@ def plain_print_dict(d, depth=0):
|
|||
print(d)
|
||||
|
||||
|
||||
def pretty_date(_date):
|
||||
"""Display a date in the current time zone without ms and tzinfo
|
||||
|
||||
Argument:
|
||||
- date -- The date or datetime to display
|
||||
"""
|
||||
# Deduce system timezone
|
||||
nowutc = datetime.now(tz=pytz.utc)
|
||||
nowtz = datetime.now()
|
||||
nowtz = nowtz.replace(tzinfo=pytz.utc)
|
||||
offsetHour = nowutc - nowtz
|
||||
offsetHour = int(round(offsetHour.total_seconds() / 3600))
|
||||
localtz = 'Etc/GMT%+d' % offsetHour
|
||||
|
||||
# Transform naive date into UTC date
|
||||
if _date.tzinfo is None:
|
||||
_date = _date.replace(tzinfo=pytz.utc)
|
||||
|
||||
# Convert UTC date into system locale date
|
||||
_date = _date.astimezone(pytz.timezone(localtz))
|
||||
if isinstance(_date, datetime):
|
||||
return _date.strftime("%Y-%m-%d %H:%M:%S")
|
||||
else:
|
||||
return _date.strftime("%Y-%m-%d")
|
||||
|
||||
|
||||
def pretty_print_dict(d, depth=0):
|
||||
"""Print in a pretty way a dictionary recursively
|
||||
|
||||
|
@ -127,10 +155,14 @@ def pretty_print_dict(d, depth=0):
|
|||
else:
|
||||
if isinstance(value, unicode):
|
||||
value = value.encode('utf-8')
|
||||
elif isinstance(v, date):
|
||||
v = pretty_date(v)
|
||||
print("{:s}- {}".format(" " * (depth + 1), value))
|
||||
else:
|
||||
if isinstance(v, unicode):
|
||||
v = v.encode('utf-8')
|
||||
elif isinstance(v, date):
|
||||
v = pretty_date(v)
|
||||
print("{:s}{}: {}".format(" " * depth, k, v))
|
||||
|
||||
|
||||
|
@ -145,6 +177,7 @@ def get_locale():
|
|||
# CLI Classes Implementation -------------------------------------------
|
||||
|
||||
class TTYHandler(logging.StreamHandler):
|
||||
|
||||
"""TTY log handler
|
||||
|
||||
A handler class which prints logging records for a tty. The record is
|
||||
|
@ -210,6 +243,7 @@ class TTYHandler(logging.StreamHandler):
|
|||
|
||||
|
||||
class ActionsMapParser(BaseActionsMapParser):
|
||||
|
||||
"""Actions map's Parser for the CLI
|
||||
|
||||
Provide actions map parsing methods for a CLI usage. The parser for
|
||||
|
@ -329,7 +363,7 @@ class ActionsMapParser(BaseActionsMapParser):
|
|||
raise
|
||||
except:
|
||||
logger.exception("unable to parse arguments '%s'", ' '.join(args))
|
||||
raise MoulinetteError(errno.EINVAL, m18n.g('error_see_log'))
|
||||
raise MoulinetteError('error_see_log')
|
||||
else:
|
||||
self.prepare_action_namespace(getattr(ret, '_tid', None), ret)
|
||||
self._parser.dequeue_callbacks(ret)
|
||||
|
@ -337,6 +371,7 @@ class ActionsMapParser(BaseActionsMapParser):
|
|||
|
||||
|
||||
class Interface(BaseInterface):
|
||||
|
||||
"""Command-line Interface for the moulinette
|
||||
|
||||
Initialize an interface connected to the standard input/output
|
||||
|
@ -376,7 +411,7 @@ class Interface(BaseInterface):
|
|||
|
||||
"""
|
||||
if output_as and output_as not in ['json', 'plain', 'none']:
|
||||
raise MoulinetteError(errno.EINVAL, m18n.g('invalid_usage'))
|
||||
raise MoulinetteError('invalid_usage')
|
||||
|
||||
# auto-complete
|
||||
argcomplete.autocomplete(self.actionsmap.parser._parser)
|
||||
|
@ -389,7 +424,7 @@ class Interface(BaseInterface):
|
|||
try:
|
||||
ret = self.actionsmap.process(args, timeout=timeout)
|
||||
except (KeyboardInterrupt, EOFError):
|
||||
raise MoulinetteError(errno.EINTR, m18n.g('operation_interrupted'))
|
||||
raise MoulinetteError('operation_interrupted')
|
||||
|
||||
if ret is None or output_as == 'none':
|
||||
return
|
||||
|
@ -439,7 +474,7 @@ class Interface(BaseInterface):
|
|||
if confirm:
|
||||
m = message[0].lower() + message[1:]
|
||||
if prompt(m18n.g('confirm', prompt=m)) != value:
|
||||
raise MoulinetteError(errno.EINVAL, m18n.g('values_mismatch'))
|
||||
raise MoulinetteError('values_mismatch')
|
||||
|
||||
return value
|
||||
|
||||
|
|
|
@ -23,21 +23,16 @@ def read_file(file_path):
|
|||
|
||||
# Check file exists
|
||||
if not os.path.isfile(file_path):
|
||||
raise MoulinetteError(errno.ENOENT,
|
||||
m18n.g('file_not_exist', path=file_path))
|
||||
raise MoulinetteError('file_not_exist', path=file_path)
|
||||
|
||||
# Open file and read content
|
||||
try:
|
||||
with open(file_path, "r") as f:
|
||||
file_content = f.read()
|
||||
except IOError as e:
|
||||
raise MoulinetteError(errno.EACCES,
|
||||
m18n.g('cannot_open_file',
|
||||
file=file_path, error=str(e)))
|
||||
raise MoulinetteError('cannot_open_file', file=file_path, error=str(e))
|
||||
except Exception as e:
|
||||
raise MoulinetteError(errno.EIO,
|
||||
m18n.g('error_reading_file',
|
||||
file=file_path, error=str(e)))
|
||||
raise MoulinetteError('error_reading_file', file=file_path, error=str(e))
|
||||
|
||||
return file_content
|
||||
|
||||
|
@ -57,9 +52,7 @@ def read_json(file_path):
|
|||
try:
|
||||
loaded_json = json.loads(file_content)
|
||||
except ValueError as e:
|
||||
raise MoulinetteError(errno.EINVAL,
|
||||
m18n.g('corrupted_json',
|
||||
ressource=file_path, error=str(e)))
|
||||
raise MoulinetteError('corrupted_json', ressource=file_path, error=str(e))
|
||||
|
||||
return loaded_json
|
||||
|
||||
|
@ -79,9 +72,7 @@ def read_yaml(file_path):
|
|||
try:
|
||||
loaded_yaml = yaml.safe_load(file_content)
|
||||
except ValueError as e:
|
||||
raise MoulinetteError(errno.EINVAL,
|
||||
m18n.g('corrupted_yaml',
|
||||
ressource=file_path, error=str(e)))
|
||||
raise MoulinetteError('corrupted_yaml', ressource=file_path, error=str(e))
|
||||
|
||||
return loaded_yaml
|
||||
|
||||
|
@ -111,13 +102,9 @@ def write_to_file(file_path, data, file_mode="w"):
|
|||
with open(file_path, file_mode) as f:
|
||||
f.write(data)
|
||||
except IOError as e:
|
||||
raise MoulinetteError(errno.EACCES,
|
||||
m18n.g('cannot_write_file',
|
||||
file=file_path, error=str(e)))
|
||||
raise MoulinetteError('cannot_write_file', file=file_path, error=str(e))
|
||||
except Exception as e:
|
||||
raise MoulinetteError(errno.EIO,
|
||||
m18n.g('error_writing_file',
|
||||
file=file_path, error=str(e)))
|
||||
raise MoulinetteError('error_writing_file', file=file_path, error=str(e))
|
||||
|
||||
|
||||
def append_to_file(file_path, data):
|
||||
|
@ -152,16 +139,12 @@ def write_to_json(file_path, data):
|
|||
with open(file_path, "w") as f:
|
||||
json.dump(data, f)
|
||||
except IOError as e:
|
||||
raise MoulinetteError(errno.EACCES,
|
||||
m18n.g('cannot_write_file',
|
||||
file=file_path, error=str(e)))
|
||||
raise MoulinetteError('cannot_write_file', file=file_path, error=str(e))
|
||||
except Exception as e:
|
||||
raise MoulinetteError(errno.EIO,
|
||||
m18n.g('_error_writing_file',
|
||||
file=file_path, error=str(e)))
|
||||
raise MoulinetteError('_error_writing_file', file=file_path, error=str(e))
|
||||
|
||||
|
||||
def mkdir(path, mode=0777, parents=False, uid=None, gid=None, force=False):
|
||||
def mkdir(path, mode=0o777, parents=False, uid=None, gid=None, force=False):
|
||||
"""Create a directory with optional features
|
||||
|
||||
Create a directory and optionaly set its permissions to mode and its
|
||||
|
@ -223,16 +206,14 @@ def chown(path, uid=None, gid=None, recursive=False):
|
|||
try:
|
||||
uid = getpwnam(uid).pw_uid
|
||||
except KeyError:
|
||||
raise MoulinetteError(errno.EINVAL,
|
||||
m18n.g('unknown_user', user=uid))
|
||||
raise MoulinetteError('unknown_user', user=uid)
|
||||
elif uid is None:
|
||||
uid = -1
|
||||
if isinstance(gid, basestring):
|
||||
try:
|
||||
gid = grp.getgrnam(gid).gr_gid
|
||||
except KeyError:
|
||||
raise MoulinetteError(errno.EINVAL,
|
||||
m18n.g('unknown_group', group=gid))
|
||||
raise MoulinetteError('unknown_group', group=gid)
|
||||
elif gid is None:
|
||||
gid = -1
|
||||
|
||||
|
@ -245,9 +226,7 @@ def chown(path, uid=None, gid=None, recursive=False):
|
|||
for f in files:
|
||||
os.chown(os.path.join(root, f), uid, gid)
|
||||
except Exception as e:
|
||||
raise MoulinetteError(errno.EIO,
|
||||
m18n.g('error_changing_file_permissions',
|
||||
path=path, error=str(e)))
|
||||
raise MoulinetteError('error_changing_file_permissions', path=path, error=str(e))
|
||||
|
||||
|
||||
def chmod(path, mode, fmode=None, recursive=False):
|
||||
|
@ -271,9 +250,7 @@ def chmod(path, mode, fmode=None, recursive=False):
|
|||
for f in files:
|
||||
os.chmod(os.path.join(root, f), fmode)
|
||||
except Exception as e:
|
||||
raise MoulinetteError(errno.EIO,
|
||||
m18n.g('error_changing_file_permissions',
|
||||
path=path, error=str(e)))
|
||||
raise MoulinetteError('error_changing_file_permissions', path=path, error=str(e))
|
||||
|
||||
|
||||
def rm(path, recursive=False, force=False):
|
||||
|
@ -292,6 +269,4 @@ def rm(path, recursive=False, force=False):
|
|||
os.remove(path)
|
||||
except OSError as e:
|
||||
if not force:
|
||||
raise MoulinetteError(errno.EIO,
|
||||
m18n.g('error_removing',
|
||||
path=path, error=str(e)))
|
||||
raise MoulinetteError('error_removing', path=path, error=str(e))
|
||||
|
|
|
@ -70,6 +70,7 @@ def getHandlersByClass(classinfo, limit=0):
|
|||
|
||||
|
||||
class MoulinetteLogger(Logger):
|
||||
|
||||
"""Custom logger class
|
||||
|
||||
Extend base Logger class to provide the SUCCESS custom log level with
|
||||
|
@ -153,6 +154,7 @@ def getActionLogger(name=None, logger=None, action_id=None):
|
|||
|
||||
|
||||
class ActionFilter(object):
|
||||
|
||||
"""Extend log record for an optionnal action
|
||||
|
||||
Filter a given record and look for an `action_id` key. If it is not found
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
import errno
|
||||
import json
|
||||
|
||||
from moulinette import m18n
|
||||
from moulinette.core import MoulinetteError
|
||||
|
||||
|
||||
|
@ -25,27 +23,22 @@ def download_text(url, timeout=30, expected_status_code=200):
|
|||
r = requests.get(url, timeout=timeout)
|
||||
# Invalid URL
|
||||
except requests.exceptions.ConnectionError:
|
||||
raise MoulinetteError(errno.EBADE,
|
||||
m18n.g('invalid_url', url=url))
|
||||
raise MoulinetteError('invalid_url', url=url)
|
||||
# SSL exceptions
|
||||
except requests.exceptions.SSLError:
|
||||
raise MoulinetteError(errno.EBADE,
|
||||
m18n.g('download_ssl_error', url=url))
|
||||
raise MoulinetteError('download_ssl_error', url=url)
|
||||
# Timeout exceptions
|
||||
except requests.exceptions.Timeout:
|
||||
raise MoulinetteError(errno.ETIME,
|
||||
m18n.g('download_timeout', url=url))
|
||||
raise MoulinetteError('download_timeout', url=url)
|
||||
# Unknown stuff
|
||||
except Exception as e:
|
||||
raise MoulinetteError(errno.ECONNRESET,
|
||||
m18n.g('download_unknown_error',
|
||||
url=url, error=str(e)))
|
||||
raise MoulinetteError('download_unknown_error',
|
||||
url=url, error=str(e))
|
||||
# Assume error if status code is not 200 (OK)
|
||||
if expected_status_code is not None \
|
||||
and r.status_code != expected_status_code:
|
||||
raise MoulinetteError(errno.EBADE,
|
||||
m18n.g('download_bad_status_code',
|
||||
url=url, code=str(r.status_code)))
|
||||
raise MoulinetteError('download_bad_status_code',
|
||||
url=url, code=str(r.status_code))
|
||||
|
||||
return r.text
|
||||
|
||||
|
@ -66,7 +59,6 @@ def download_json(url, timeout=30, expected_status_code=200):
|
|||
try:
|
||||
loaded_json = json.loads(text)
|
||||
except ValueError:
|
||||
raise MoulinetteError(errno.EINVAL,
|
||||
m18n.g('corrupted_json', ressource=url))
|
||||
raise MoulinetteError('corrupted_json', ressource=url)
|
||||
|
||||
return loaded_json
|
||||
|
|
|
@ -60,12 +60,13 @@ def call_async_output(args, callback, **kwargs):
|
|||
if "stdinfo" in kwargs and kwargs["stdinfo"] is not None:
|
||||
assert len(callback) == 3
|
||||
stdinfo = kwargs.pop("stdinfo")
|
||||
os.mkfifo(stdinfo, 0600)
|
||||
os.mkfifo(stdinfo, 0o600)
|
||||
# Open stdinfo for reading (in a nonblocking way, i.e. even
|
||||
# if command does not write in the stdinfo pipe...)
|
||||
stdinfo_f = os.open(stdinfo, os.O_RDONLY | os.O_NONBLOCK)
|
||||
else:
|
||||
kwargs.pop("stdinfo")
|
||||
if "stdinfo" in kwargs:
|
||||
kwargs.pop("stdinfo")
|
||||
stdinfo = None
|
||||
|
||||
# Validate callback argument
|
||||
|
@ -98,13 +99,15 @@ def call_async_output(args, callback, **kwargs):
|
|||
# this way is not 100% perfect but should do it
|
||||
stdout_consum.process_next_line()
|
||||
stderr_consum.process_next_line()
|
||||
stdinfo_consum.process_next_line()
|
||||
if stdinfo:
|
||||
stdinfo_consum.process_next_line()
|
||||
time.sleep(.1)
|
||||
stderr_reader.join()
|
||||
# clear the queues
|
||||
stdout_consum.process_current_queue()
|
||||
stderr_consum.process_current_queue()
|
||||
stdinfo_consum.process_current_queue()
|
||||
if stdinfo:
|
||||
stdinfo_consum.process_current_queue()
|
||||
else:
|
||||
while not stdout_reader.eof():
|
||||
stdout_consum.process_current_queue()
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import logging
|
||||
from json.encoder import JSONEncoder
|
||||
import datetime
|
||||
import pytz
|
||||
|
||||
logger = logging.getLogger('moulinette.utils.serialize')
|
||||
|
||||
|
@ -8,6 +9,7 @@ logger = logging.getLogger('moulinette.utils.serialize')
|
|||
# JSON utilities -------------------------------------------------------
|
||||
|
||||
class JSONExtendedEncoder(JSONEncoder):
|
||||
|
||||
"""Extended JSON encoder
|
||||
|
||||
Extend default JSON encoder to recognize more types and classes. It
|
||||
|
@ -26,7 +28,9 @@ class JSONExtendedEncoder(JSONEncoder):
|
|||
return list(o)
|
||||
|
||||
# Display the date in its iso format ISO-8601 Internet Profile (RFC 3339)
|
||||
if isinstance(o, datetime.datetime) or isinstance(o, datetime.date):
|
||||
if isinstance(o, datetime.date):
|
||||
if o.tzinfo is None:
|
||||
o = o.replace(tzinfo=pytz.utc)
|
||||
return o.isoformat()
|
||||
|
||||
# Return the repr for object that json can't encode
|
||||
|
|
|
@ -8,6 +8,7 @@ from multiprocessing.queues import SimpleQueue
|
|||
# Read from a stream ---------------------------------------------------
|
||||
|
||||
class AsynchronousFileReader(Process):
|
||||
|
||||
"""
|
||||
Helper class to implement asynchronous reading of a file
|
||||
in a separate thread. Pushes read lines on a queue to
|
||||
|
@ -74,6 +75,7 @@ class AsynchronousFileReader(Process):
|
|||
|
||||
|
||||
class Consummer(object):
|
||||
|
||||
def __init__(self, queue, callback):
|
||||
self.queue = queue
|
||||
self.callback = callback
|
||||
|
|
3
setup.py
3
setup.py
|
@ -30,5 +30,6 @@ setup(name='Moulinette',
|
|||
'moulinette.interfaces',
|
||||
'moulinette.utils',
|
||||
],
|
||||
data_files=[(LOCALES_DIR, locale_files)]
|
||||
data_files=[(LOCALES_DIR, locale_files)],
|
||||
tests_require=["pytest", "webtest"],
|
||||
)
|
||||
|
|
100
tests/test_api.py
Normal file
100
tests/test_api.py
Normal file
|
@ -0,0 +1,100 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
|
||||
from webtest import TestApp as WebTestApp
|
||||
from bottle import Bottle
|
||||
from moulinette.interfaces.api import filter_csrf
|
||||
|
||||
|
||||
URLENCODED = 'application/x-www-form-urlencoded'
|
||||
FORMDATA = 'multipart/form-data'
|
||||
TEXT = 'text/plain'
|
||||
|
||||
TYPES = [URLENCODED, FORMDATA, TEXT]
|
||||
SAFE_METHODS = ["HEAD", "GET", "PUT", "DELETE"]
|
||||
|
||||
|
||||
app = Bottle(autojson=True)
|
||||
app.install(filter_csrf)
|
||||
|
||||
|
||||
@app.get('/')
|
||||
def get_hello():
|
||||
return "Hello World!\n"
|
||||
|
||||
|
||||
@app.post('/')
|
||||
def post_hello():
|
||||
return "OK\n"
|
||||
|
||||
|
||||
@app.put('/')
|
||||
def put_hello():
|
||||
return "OK\n"
|
||||
|
||||
|
||||
@app.delete('/')
|
||||
def delete_hello():
|
||||
return "OK\n"
|
||||
|
||||
|
||||
webtest = WebTestApp(app)
|
||||
|
||||
|
||||
def test_get():
|
||||
r = webtest.get("/")
|
||||
assert r.status_code == 200
|
||||
|
||||
|
||||
def test_csrf_post():
|
||||
r = webtest.post("/", "test", expect_errors=True)
|
||||
assert r.status_code == 403
|
||||
|
||||
|
||||
def test_post_json():
|
||||
r = webtest.post("/", "test",
|
||||
headers=[("Content-Type", "application/json")])
|
||||
assert r.status_code == 200
|
||||
|
||||
|
||||
def test_csrf_post_text():
|
||||
r = webtest.post("/", "test",
|
||||
headers=[("Content-Type", "text/plain")],
|
||||
expect_errors=True)
|
||||
assert r.status_code == 403
|
||||
|
||||
|
||||
def test_csrf_post_urlencoded():
|
||||
r = webtest.post("/", "test",
|
||||
headers=[("Content-Type",
|
||||
"application/x-www-form-urlencoded")],
|
||||
expect_errors=True)
|
||||
assert r.status_code == 403
|
||||
|
||||
|
||||
def test_csrf_post_form():
|
||||
r = webtest.post("/", "test",
|
||||
headers=[("Content-Type", "multipart/form-data")],
|
||||
expect_errors=True)
|
||||
assert r.status_code == 403
|
||||
|
||||
|
||||
def test_ok_post_text():
|
||||
r = webtest.post("/", "test",
|
||||
headers=[("Content-Type", "text/plain"),
|
||||
("X-Requested-With", "XMLHttpRequest")])
|
||||
assert r.status_code == 200
|
||||
|
||||
|
||||
def test_ok_post_urlencoded():
|
||||
r = webtest.post("/", "test",
|
||||
headers=[("Content-Type",
|
||||
"application/x-www-form-urlencoded"),
|
||||
("X-Requested-With", "XMLHttpRequest")])
|
||||
assert r.status_code == 200
|
||||
|
||||
|
||||
def test_ok_post_form():
|
||||
r = webtest.post("/", "test",
|
||||
headers=[("Content-Type", "multipart/form-data"),
|
||||
("X-Requested-With", "XMLHttpRequest")])
|
||||
assert r.status_code == 200
|
Loading…
Add table
Reference in a new issue